繁体   English   中英

打字稿:嵌套对象的深度keyof,具有相关类型

[英]Typescript: deep keyof of a nested object, with related type

我正在寻找一种方法来拥有嵌套对象的所有键/值对。

(用于 MongoDB 点符号键/值类型的自动完成)

interface IPerson {
    name: string;
    age: number;
    contact: {
        address: string;
        visitDate: Date;
    }
}

这是我想要实现的目标,使其成为:

type TPerson = {
    name: string;
    age: number;
    contact: { address: string; visitDate: Date; }
    "contact.address": string;
    "contact.visitDate": Date;
}

我尝试过的:

在这个答案中,我可以使用Leaves<IPerson>获取密钥。 所以它变成了'name' | 'age' | 'contact.address' | 'contact.visitDate' 'name' | 'age' | 'contact.address' | 'contact.visitDate' 'name' | 'age' | 'contact.address' | 'contact.visitDate'

在@jcalz 的另一个答案中,我可以使用DeepIndex<IPerson, ...>获得深度相关的值类型。

是否可以将它们组合在一起,成为像TPerson这样的TPerson

修改 9/14:用例,需要和不需要:

当我开始这个问题时,我认为它可以像[K in keyof T]: T[K]; ,经过一些巧妙的改造。 但是我错了。 这是我需要的:

1. 索引签名

所以界面

interface IPerson {
    contact: {
        address: string;
        visitDate: Date;
    }[]
}

变成

type TPerson = {
    [x: `contact.${number}.address`]: string;
    [x: `contact.${number}.visitDate`]: Date;
    contact: {
        address: string;
        visitDate: Date;
    }[];
}

无需检查有效number ,数组/索引签名的性质应该允许任意数量的元素。

2.元组

界面

interface IPerson {
    contact: [string, Date]
}

变成

type TPerson = {
    [x: `contact.0`]: string;
    [x: `contact.1`]: Date;
    contact: [string, Date];
}

元组应该是关心有效索引号的那个。

3. 只读

readonly属性应该从最终结构中删除。

interface IPerson {
    readonly _id: string;
    age: number;
    readonly _created_date: Date;
}

变成

type TPerson = {
    age: number;
}

用例是针对 MongoDB,在创建数据后不能修改_id_created_date _id: never在这种情况下不起作用,因为它会阻止TPerson的创建。

4. 可选

interface IPerson {
    contact: {
        address: string;
        visitDate?: Date;
    }[];        
}

变成

type TPerson = {
    [x: `contact.${number}.address`]: string;
    [x: `contact.${number}.visitDate`]?: Date;
    contact: {
        address: string;
        visitDate?: Date;
    }[];
}

将可选标志带入转换结构就足够了。

5. 路口

interface IPerson {
    contact: { address: string; } & { visitDate: Date; }
}

变成

type TPerson = {
    [x: `contact.address`]: string;
    [x: `contact.visitDate`]?: Date;
    contact: { address: string; } & { visitDate: Date; }
}

6. 可以指定类型为异常

界面

interface IPerson {
    birth: Date;
}

变成

type TPerson = {
    birth: Date;
}

不是

type TPerson = {
    age: Date;
    "age.toDateString": () => string;
    "age.toTimeString": () => string;
    "age.toLocaleDateString": {
    ...
}

我们可以给出一个类型列表作为结束节点。

这是我不需要的:

  1. 联盟。 它可能太复杂了。
  2. 类相关关键字。 无需处理关键字 ex: private / abstract 。
  3. 其余的我没有在这里写。

为了实现这个目标,我们需要创建所有允许路径的排列。 例如:

type Structure = {
    user: {
        name: string,
        surname: string
    }
}

type BlackMagic<T>= T

// user.name | user.surname
type Result=BlackMagic<Structure>

对于数组和空元组,问题变得更有趣。

元组,显式长度的数组,应该这样管理:

type Structure = {
    user: {
        arr: [1, 2],
    }
}

type BlackMagic<T> = T

// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>

逻辑是直截了当的。 但是我们如何处理number[]呢? 不能保证索引1存在。

我决定使用user.arr.${number}

type Structure = {
    user: {
        arr: number[],
    }
}

type BlackMagic<T> = T

// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>

我们还有 1 个问题。 空元组。 具有零元素的数组 - [] 我们需要允许索引吗? 我不知道。 我决定使用-1

type Structure = {
    user: {
        arr: [],
    }
}

type BlackMagic<T> = T

//  "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>

我认为这里最重要的是一些约定。 我们也可以使用字符串化的“从不”。 我认为这取决于 OP 如何处理它。

既然我们知道我们需要如何处理不同的情况,我们就可以开始我们的实现了。 在我们继续之前,我们需要定义几个助手。

type Values<T> = T[keyof T]
{
    // 1 | "John"
    type _ = Values<{ age: 1, name: 'John' }>
}

type IsNever<T> = [T] extends [never] ? true : false;
{
    type _ = IsNever<never> // true 
    type __ = IsNever<true> // false
}

type IsTuple<T> =
    (T extends Array<any> ?
        (T['length'] extends number
            ? (number extends T['length']
                ? false
                : true)
            : true)
        : false)
{
    type _ = IsTuple<[1, 2]> // true
    type __ = IsTuple<number[]> // false
    type ___ = IsTuple<{ length: 2 }> // false
}

type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
    type _ = IsEmptyTuple<[]> // true
    type __ = IsEmptyTuple<[1]> // false
    type ___ = IsEmptyTuple<number[]> // false

}

我认为命名和测试是不言自明的。 至少我想相信:D

现在,当我们拥有所有的 utils 时,我们可以定义我们的主要 util:

/**
 * If Cache is empty return Prop without dot,
 * to avoid ".user"
 */
type HandleDot<
    Cache extends string,
    Prop extends string | number
    > =
    Cache extends ''
    ? `${Prop}`
    : `${Cache}.${Prop}`

/**
 * Simple iteration through object properties
 */
type HandleObject<Obj, Cache extends string> = {
    [Prop in keyof Obj]:
    // concat previous Cacha and Prop
    | HandleDot<Cache, Prop & string>
    // with next Cache and Prop
    | Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]

type Path<Obj, Cache extends string = ''> =
    // if Obj is primitive
    (Obj extends PropertyKey
        // return Cache
        ? Cache
        // if Obj is Array (can be array, tuple, empty tuple)
        : (Obj extends Array<unknown>
            // and is tuple
            ? (IsTuple<Obj> extends true
                // and tuple is empty
                ? (IsEmptyTuple<Obj> extends true
                    // call recursively Path with `-1` as an allowed index
                    ? Path<PropertyKey, HandleDot<Cache, -1>>
                    // if tuple is not empty we can handle it as regular object
                    : HandleObject<Obj, Cache>)
                // if Obj is regular  array call Path with union of all elements
                : Path<Obj[number], HandleDot<Cache, number>>)
            // if Obj is neither Array nor Tuple nor Primitive - treat is as object    
            : HandleObject<Obj, Cache>)
    )

// "user" | "user.arr" | `user.arr.${number}`
type Test = Extract<Path<Structure>, string>

有一个小问题。 我们不应该返回最高级别的道具,例如user 我们需要至少有一个点的路径。

有两种方式:

  • 提取所有没有点的道具
  • 为索引级别提供额外的通用参数。

两个选项很容易实现。

使用dot (.)获取所有道具:

type WithDot<T extends string> = T extends `${string}.${string}` ? T : never

虽然上面的 util 是可读和可维护的,但第二个有点难。 我们需要在PathHandleObject提供额外的通用参数。 请参阅取自其他问题/文章的此示例:

type KeysUnion<T, Cache extends string = '', Level extends any[] = []> =
  T extends PropertyKey ? Cache : {
    [P in keyof T]:
    P extends string
    ? Cache extends ''
    ? KeysUnion<T[P], `${P}`, [...Level, 1]>
    : Level['length'] extends 1 // if it is a higher level - proceed
    ? KeysUnion<T[P], `${Cache}.${P}`, [...Level, 1]>
    : Level['length'] extends 2 // stop on second level
    ? Cache | KeysUnion<T[P], `${Cache}`, [...Level, 1]>
    : never
    : never
  }[keyof T]

老实说,我认为任何人都不容易阅读此内容。

我们还需要实施一件事。 我们需要通过计算路径获得一个值。


type Acc = Record<string, any>

type ReducerCallback<Accumulator extends Acc, El extends string> =
    El extends keyof Accumulator ? Accumulator[El] : Accumulator

type Reducer<
    Keys extends string,
    Accumulator extends Acc = {}
    > =
    // Key destructure
    Keys extends `${infer Prop}.${infer Rest}`
    // call Reducer with callback, just like in JS
    ? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
    // this is the last part of path because no dot
    : Keys extends `${infer Last}`
    // call reducer with last part
    ? ReducerCallback<Accumulator, Last>
    : never

{
    type _ = Reducer<'user.arr', Structure> // []
    type __ = Reducer<'user', Structure> // { arr: [] }
}

您可以在我的博客中找到有关使用Reduce更多信息。

全码:

type Structure = {
    user: {
        tuple: [42],
        emptyTuple: [],
        array: { age: number }[]
    }
}


type Values<T> = T[keyof T]
{
    // 1 | "John"
    type _ = Values<{ age: 1, name: 'John' }>
}

type IsNever<T> = [T] extends [never] ? true : false;
{
    type _ = IsNever<never> // true 
    type __ = IsNever<true> // false
}

type IsTuple<T> =
    (T extends Array<any> ?
        (T['length'] extends number
            ? (number extends T['length']
                ? false
                : true)
            : true)
        : false)
{
    type _ = IsTuple<[1, 2]> // true
    type __ = IsTuple<number[]> // false
    type ___ = IsTuple<{ length: 2 }> // false
}

type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
    type _ = IsEmptyTuple<[]> // true
    type __ = IsEmptyTuple<[1]> // false
    type ___ = IsEmptyTuple<number[]> // false
}

/**
 * If Cache is empty return Prop without dot,
 * to avoid ".user"
 */
type HandleDot<
    Cache extends string,
    Prop extends string | number
    > =
    Cache extends ''
    ? `${Prop}`
    : `${Cache}.${Prop}`

/**
 * Simple iteration through object properties
 */
type HandleObject<Obj, Cache extends string> = {
    [Prop in keyof Obj]:
    // concat previous Cacha and Prop
    | HandleDot<Cache, Prop & string>
    // with next Cache and Prop
    | Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]

type Path<Obj, Cache extends string = ''> =
    (Obj extends PropertyKey
        // return Cache
        ? Cache
        // if Obj is Array (can be array, tuple, empty tuple)
        : (Obj extends Array<unknown>
            // and is tuple
            ? (IsTuple<Obj> extends true
                // and tuple is empty
                ? (IsEmptyTuple<Obj> extends true
                    // call recursively Path with `-1` as an allowed index
                    ? Path<PropertyKey, HandleDot<Cache, -1>>
                    // if tuple is not empty we can handle it as regular object
                    : HandleObject<Obj, Cache>)
                // if Obj is regular  array call Path with union of all elements
                : Path<Obj[number], HandleDot<Cache, number>>)
            // if Obj is neither Array nor Tuple nor Primitive - treat is as object    
            : HandleObject<Obj, Cache>)
    )

type WithDot<T extends string> = T extends `${string}.${string}` ? T : never


// "user" | "user.arr" | `user.arr.${number}`
type Test = WithDot<Extract<Path<Structure>, string>>



type Acc = Record<string, any>

type ReducerCallback<Accumulator extends Acc, El extends string> =
    El extends keyof Accumulator ? Accumulator[El] : El extends '-1' ? never : Accumulator

type Reducer<
    Keys extends string,
    Accumulator extends Acc = {}
    > =
    // Key destructure
    Keys extends `${infer Prop}.${infer Rest}`
    // call Reducer with callback, just like in JS
    ? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
    // this is the last part of path because no dot
    : Keys extends `${infer Last}`
    // call reducer with last part
    ? ReducerCallback<Accumulator, Last>
    : never

{
    type _ = Reducer<'user.arr', Structure> // []
    type __ = Reducer<'user', Structure> // { arr: [] }
}

type BlackMagic<T> = T & {
    [Prop in WithDot<Extract<Path<T>, string>>]: Reducer<Prop, T>
}

type Result = BlackMagic<Structure>

操场

下面是我对Flatten<T, O>的完整实现Flatten<T, O>它将类型可能嵌套的T转换为“扁平”版本,其键是通过原始T的虚线路径 O类型是一个可选类型,您可以在其中指定一个(联合)对象类型以保持原样而不将它们展平。 在您的示例中,这只是Date ,但您可以有其他类型。

警告:它非常丑陋,而且可能很脆弱。 到处都有边缘情况。 组成它的部分涉及奇怪的类型操作,这些操作要么并不总是按照人们的预期进行,要么对于除最经验丰富的 TypeScript 老手之外的所有人来说都是不可理解的,或者两者兼而有之。

有鉴于此,除了可能“请不要这样做”之外,对这个问题没有“规范”的答案。 但我很高兴展示我的版本。

这里是:


type Flatten<T, O = never> = Writable<Cleanup<T>, O> extends infer U ?
    U extends O ? U : U extends object ?
    ValueOf<{ [K in keyof U]-?: (x: PrefixKeys<Flatten<U[K], O>, K, O>) => void }>
    | ((x: U) => void) extends (x: infer I) => void ?
    { [K in keyof I]: I[K] } : never : U : never;

这里的基本方法是采用您的T类型,如果它不是一个对象或者它扩展了O ,则按原样返回它。 否则,我们删除任何readonly属性,并将任何数组或元组转换为没有所有数组方法(如push()map() )的版本并获得U 然后我们将其中的每个属性展平。 我们有一个键K和一个扁平属性Flatten<U[K]> 我们想在Flatten<U[K]>的虚线路径前面添加键K ,当我们完成所有这些后,我们希望将这些扁平对象(也包括未扁平对象) 相交成为一个大对象.

请注意,说服编译器生成交集涉及逆变位置中的条件类型推断(请参阅将联合类型转换为交集类型),即(x: XXX) => void)extends (x: infer I) => void件进来。它使编译器采用所有不同的XXX值并将它们相交以获得I

虽然像{foo: string} & {bar: number} & {baz: boolean}这样的交集是我们在概念上想要的,但它比等效的{foo: string; bar: number; baz: boolean} {foo: string; bar: number; baz: boolean} {foo: string; bar: number; baz: boolean}所以我做一些更多的条件类型映射{ [K in keyof I]: I[K] }而不是只是I (看我如何可以看到一个打字稿类型的全扩展的合约? )。

此代码通常分布在 unions 上,因此可选属性可能最终会产生 unions(例如{a?: {b: string}}可能会产生{"ab": string; a?: {b: string}} | {"a": undefined, a?: {b: string}} ,虽然这可能不是您想要的表示形式,但它应该可以工作(因为,例如,如果a是可选的, "ab"可能不作为键存在)。


Flatten定义取决于辅助类型函数,我将在此处以各种级别的描述呈现这些函数:

type Writable<T, O> = T extends O ? T : {
    [P in keyof T as IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>]: T[P]
}

type IfEquals<X, Y, A = X, B = never> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? A : B;

Writable<T, O>返回一个删除了readonly属性的T版本(除非T extends O在这种情况下我们不理会它)。 它来自TypeScript 条件类型——过滤掉只读属性/只选择需要的属性

下一个:

type Cleanup<T> =
    0 extends (1 & T) ? unknown :
    T extends readonly any[] ?
    (Exclude<keyof T, keyof any[]> extends never ?
        { [k: `${number}`]: T[number] } : Omit<T, keyof any[]>) : T;

Cleanup<T>类型将any类型转换unknown类型(因为any类型操作any非常混乱),将元组转换为具有单个数字键( "0""1"等)的对象,并将其他数组转换为只是一个索引签名

下一个:

type PrefixKeys<V, K extends PropertyKey, O> =
    V extends O ? { [P in K]: V } : V extends object ?
    { [P in keyof V as
        `${Extract<K, string | number>}.${Extract<P, string | number>}`]: V[P] } :
    { [P in K]: V };

PrefixKeys<V, K, O>将键K PrefixKeys<V, K, O>V的属性键中的路径......除非V扩展OV不是对象。 它使用模板文字类型来做到这一点。

最后:

type ValueOf<T> = T[keyof T]

将类型T转换为其属性的联合。 请参见TypeScript 中是否有类似于 `keyof` 的 `valueof`? .

哇! 😅


所以,你去了。 您可以验证这与您陈述的用例的紧密程度。 但它非常复杂和脆弱,我真的不建议在没有大量测试的任何生产代码环境中使用它。

Playground 链接到代码

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM