代码之家  ›  专栏  ›  技术社区  ›  Eliya Cohen

如何通过嵌套对象推断类型

  •  0
  • Eliya Cohen  · 技术社区  · 4 年前

    因此,我想找到一种方法来拥有嵌套对象的所有键。

    我有一个泛型类型,它在参数中接受一个类型。我的目标是获得给定类型的所有密钥。

    以下代码在这种情况下运行良好。但当我开始使用嵌套对象时,情况就不同了。

    type SimpleObjectType = {
      a: string;
      b: string;
    };
    
    // works well for a simple object
    type MyGenericType<T extends object> = {
      keys: Array<keyof T>;
    };
    
    const test: MyGenericType<SimpleObjectType> = {
      keys: ['a'];
    }
    

    这就是我想要实现的,但它不起作用。

    type NestedObjectType = {
      a: string;
      b: string;
      nest: {
        c: string;
      };
      otherNest: {
        c: string;
      };
    };
    
    type MyGenericType<T extends object> = {
      keys: Array<keyof T>;
    };
    
    // won't works => Type 'string' is not assignable to type 'a' | 'b' | 'nest' | 'otherNest'
    const test: MyGenericType<NestedObjectType> = {
      keys: ['a', 'nest.c'];
    }
    

    那么,在不使用函数的情况下,我该怎么办才能给 test ?

    0 回复  |  直到 5 年前
        1
  •  7
  •   Joaquín Michelet    2 年前

    目前,不必担心边缘情况的最简单方法如下

    type Paths<T> = T extends object ? { [K in keyof T]:
      `${Exclude<K, symbol>}${"" | `.${Paths<T[K]>}`}`
    }[keyof T] : never
    
    type Leaves<T> = T extends object ? { [K in keyof T]:
      `${Exclude<K, symbol>}${Leaves<T[K]> extends never ? "" : `.${Leaves<T[K]>}`}`
    }[keyof T] : never
    

    它产生

    type NestedObjectType = {
      a: string; b: string;
      nest: { c: string; };
      otherNest: { c: string; };
    };
    
    type NestedObjectPaths = Paths<NestedObjectType>
    // type NestedObjectPaths = "a" | "b" | "nest" | 
    //   "otherNest" | "nest.c" | "otherNest.c"
    
    type NestedObjectLeaves = Leaves<NestedObjectType>
    // type NestedObjectLeaves = "a" | "b" | "nest.c" | "otherNest.c"
    

    Playground link to code


    TS4.1的更新 现在可以在类型级别连接字符串文字,使用 模板文字类型 如实施于 microsoft/TypeScript#40336 。下面的实现可以进行调整以使用它,而不是类似的东西 Cons (其本身可以使用 可变元组类型 作为 introduced in TypeScript 4.0 ):

    type Join<K, P> = K extends string | number ?
        P extends string | number ?
        `${K}${"" extends P ? "" : "."}${P}`
        : never : never;
    

    在这里 Join 除非最后一个字符串为空,否则将两个字符串连接起来,中间有一个点。所以 Join<"a","b.c"> "a.b.c" 虽然 Join<"a",""> "a" .

    那么 Paths Leaves 成为:

    type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
        { [K in keyof T]-?: K extends string | number ?
            `${K}` | Join<K, Paths<T[K], Prev[D]>>
            : never
        }[keyof T] : ""
    
    type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
        { [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T] : "";
    

    其他类型则从中脱颖而出:

    type NestedObjectPaths = Paths<NestedObjectType>;
    // type NestedObjectPaths = "a" | "b" | "nest" | "otherNest" | "nest.c" | "otherNest.c"
    type NestedObjectLeaves = Leaves<NestedObjectType>
    // type NestedObjectLeaves = "a" | "b" | "nest.c" | "otherNest.c"
    

    type MyGenericType<T extends object> = {
        keys: Array<Paths<T>>;
    };
    
    const test: MyGenericType<NestedObjectType> = {
        keys: ["a", "nest.c"]
    }
    

    其余的答案基本相同。递归条件类型(如中实现的 microsoft/TypeScript#40002 )TS4.1也将支持,但递归限制仍然适用,因此没有深度限制器的树状结构会出现问题,例如 Prev .

    请注意,这将使非点表键形成虚线路径,如 {foo: [{"bar-baz": 1}]} 可能产生 foo.0.bar-baz 。因此,请注意避免使用这样的键,或者重写上述内容以排除它们。

    另请注意:这些递归类型本质上是“棘手的”,如果稍加修改,往往会让编译器不满意。如果你不够幸运,你会看到诸如“类型实例化太深”之类的错误,如果你 非常 不幸的是,你会看到编译器耗尽你所有的CPU,永远无法完成类型检查。总的来说,我不知道该怎么说这种问题。。。只是这样的事情有时比它们的价值更麻烦。

    Playground link to code



    TS4.1之前的答案:

    如上所述,目前无法在类型级别连接字符串文字。有一些建议可能允许这样做,例如 a suggestion to allow augmenting keys during mapped types a suggestion to validate string literals via regular expression ,但目前这是不可能的。

    您可以将路径表示为 tuples 字符串文字。所以 成为 ["a"] ,以及 "nest.c" 成为 ["nest", "c"] 在运行时,通过以下方式在这些类型之间转换很容易 split() join() 方法。


    所以你可能想要这样的东西 Paths<T> 它返回给定类型的所有路径的并集 T ,或者可能 Leaves<T> 这只是这些元素 路径<T> 它们指向非对象类型本身。这种类型没有内置支持;这个 ts-toolbelt 图书馆 has this ,但因为我不能用那个图书馆 Playground ,我会在这里滚自己的。

    请注意: 路径 叶子 本质上是递归的,这对编译器来说可能非常费力。还有 recursive types of the sort needed for this not officially supported 在TypeScript中也是如此。下面我将以这种不确定/不受支持的方式呈现递归,但我试图为您提供一种指定最大递归深度的方法。

    我们开始吧:

    type Cons<H, T> = T extends readonly any[] ?
        ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
        : never;
    
    type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
    
    type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
        { [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
            P extends [] ? never : Cons<K, P> : never
        ) }[keyof T]
        : [];
    
    
    type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
        { [K in keyof T]-?: Cons<K, Leaves<T[K], Prev[D]>> }[keyof T]
        : [];
    

    意图 Cons<H, T> 可以采取任何类型 H 以及元组类型 T 并生成一个新的元组 H 已添加到 T 所以 Cons<1, [2,3,4]> 应该是 [1,2,3,4] .实施使用 rest/spread tuples 我们需要这个来建立路径。

    类型 上一个 是一个长元组,可用于获取前一个数字(最大值)。所以 Prev[10] 9 ,以及 Prev[1] 0 。当我们深入对象树时,我们需要它来限制递归。

    最后, Paths<T, D> Leaves<T, D> 通过深入每种对象类型来实现 T 并收集密钥,以及 欺骗 把它们放到 路径 叶子 这些按键上的属性。它们之间的区别在于 路径 还直接将子路径包含在联合中。默认情况下,深度参数 D 10 ,每往下一步,我们都会减少 D 一个,直到我们试图过去 0 ,此时我们停止重复。


    好吧,让我们测试一下:

    type NestedObjectPaths = Paths<NestedObjectType>;
    // type NestedObjectPaths = [] | ["a"] | ["b"] | ["c"] | 
    // ["nest"] | ["nest", "c"] | ["otherNest"] | ["otherNest", "c"]
    type NestedObjectLeaves = Leaves<NestedObjectType>
    // type NestedObjectLeaves = ["a"] | ["b"] | ["nest", "c"] | ["otherNest", "c"]
    

    为了看到深度限制的有用性,想象一下我们有一个这样的树类型:

    interface Tree {
        left: Tree,
        right: Tree,
        data: string
    }
    

    好, Leaves<Tree> 嗯,很大:

    type TreeLeaves = Leaves<Tree>; // sorry, compiler 💻⌛😫
    // type TreeLeaves = ["data"] | ["left", "data"] | ["right", "data"] | 
    // ["left", "left", "data"] | ["left", "right", "data"] | 
    // ["right", "left", "data"] | ["right", "right", "data"] | 
    // ["left", "left", "left", "data"] | ... 2038 more ... | [...]
    

    编译器生成它需要很长时间,编辑器的性能会突然变得非常差。让我们将其限制在更易于管理的范围内:

    type TreeLeaves = Leaves<Tree, 3>;
    // type TreeLeaves2 = ["data"] | ["left", "data"] | ["right", "data"] |
    // ["left", "left", "data"] | ["left", "right", "data"] | 
    // ["right", "left", "data"] | ["right", "right", "data"]
    

    这迫使编译器停止查看深度为3的路径,因此所有路径的长度最多为3。


    所以,这是有效的。ts工具带或其他实现很可能会更加小心,以免导致编译器心脏病发作。因此,我不一定说你应该在没有进行大量测试的情况下在生产代码中使用它。

    但不管怎样,这是你想要的类型,假设你有并且想要 路径 :

    type MyGenericType<T extends object> = {
        keys: Array<Paths<T>>;
    };
    
    const test: MyGenericType<NestedObjectType> = {
        keys: [['a'], ['nest', 'c']]
    }
    

    Link to code

        2
  •  6
  •   Albert Mañosa    1 年前

    一个递归类型函数,使用 conditional types , template literal 串, mapped types index access types 基于 @jcalz's answer 并且可以用这个来验证 ts playground example

    生成一种联合类型的属性,包括用点表示法嵌套的属性

    type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`
    
    type DotNestedKeys<T> = (T extends object ?
        { [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DotNestedKeys<T[K]>>}` }[Exclude<keyof T, symbol>]
        : "") extends infer D ? Extract<D, string> : never;
    
    /* testing */
    
    type NestedObjectType = {
        a: string
        b: string
        nest: {
            c: string;
        }
        otherNest: {
            c: string;
        }
    }
    
    type NestedObjectKeys = DotNestedKeys<NestedObjectType>
    // type NestedObjectKeys = "a" | "b" | "nest.c" | "otherNest.c"
    
    const test2: Array<NestedObjectKeys> = ["a", "b", "nest.c", "otherNest.c"]
    

    这在使用文档数据库时也很有用,例如 mongodb firebase firestore 它允许使用点符号设置单个嵌套属性

    使用mongodb

    db.collection("products").update(
       { _id: 100 },
       { $set: { "details.make": "zzz" } }
    )
    

    使用firebase

    db.collection("users").doc("frank").update({
       "age": 13,
       "favorites.color": "Red"
    })
    

    可以使用此类型创建此更新对象

    然后typescript会引导你,只需添加你需要的属性

    export type DocumentUpdate<T> = Partial<{ [key in DotNestedKeys<T>]: any & T}> & Partial<T>
    

    enter image description here

    您还可以更新do嵌套属性生成器,以避免显示嵌套属性数组、日期。。。

    type DotNestedKeys<T> =
    T extends (ObjectId | Date | Function | Array<any>) ? "" :
    (T extends object ?
        { [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DotNestedKeys<T[K]>>}` }[Exclude<keyof T, symbol>]
        : "") extends infer D ? Extract<D, string> : never;
    
        3
  •  4
  •   wangzi    2 年前

    我遇到了一个类似的问题,当然,上面的答案非常惊人。但对我来说,它有点过头了,如上所述,对编译器来说相当费力。

    虽然不那么优雅,但阅读起来要简单得多,我建议使用以下类型来生成类似Path的元组:

    type PathTree<T> = {
        [P in keyof T]-?: T[P] extends object
            ? [P] | [P, ...Path<T[P]>]
            : [P];
    };
    
    type Path<T> = PathTree<T>[keyof T];
    

    一个主要的缺点是,这种类型不能处理像这样的自引用类型 Tree 来自@jcalz的回答:

    interface Tree {
      left: Tree,
      right: Tree,
      data: string
    };
    
    type TreePath = Path<Tree>;
    // Type of property 'left' circularly references itself in mapped type 'PathTree<Tree>'.ts(2615)
    // Type of property 'right' circularly references itself in mapped type 'PathTree<Tree>'.ts(2615)
    

    但对于其他类型,它似乎做得很好:

    interface OtherTree {
      nested: {
        props: {
          a: string,
          b: string,
        }
        d: number,
      }
      e: string
    };
    
    type OtherTreePath = Path<OtherTree>;
    // ["nested"] | ["nested", "props"] | ["nested", "props", "a"]
    // | ["nested", "props", "b"] | ["nested", "d"] | ["e"]
    

    如果只想强制引用叶节点,可以删除 [P] | PathTree 类型:

    type LeafPathTree<T> = {
        [P in keyof T]-?: T[P] extends object 
            ? [P, ...LeafPath<T[P]>]
            : [P];
    };
    type LeafPath<T> = LeafPathTree<T>[keyof T];
    
    type OtherPath = Path<OtherTree>;
    // ["nested", "props", "a"] | ["nested", "props", "b"] | ["nested", "d"] | ["e"]
    

    不幸的是,对于一些更复杂的对象,该类型似乎默认为 [...any[]] .


    当您需要类似于的点语法时 @Alonso's 答案是,你可以将元组映射到模板字符串类型:

    // Yes, not pretty, but not much you can do about it at the moment
    // Supports up to depth 10, more can be added if needed
    type Join<T extends (string | number)[], D extends string = '.'> =
      T extends { length: 1 } ? `${T[0]}`
      : T extends { length: 2 } ? `${T[0]}${D}${T[1]}`
      : T extends { length: 3 } ? `${T[0]}${D}${T[1]}${D}${T[2]}`
      : T extends { length: 4 } ? `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}`
      : T extends { length: 5 } ? `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}${D}${T[4]}`
      : T extends { length: 6 } ? `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}${D}${T[4]}${D}${T[5]}`
      : T extends { length: 7 } ? `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}${D}${T[4]}${D}${T[5]}${D}${T[6]}`
      : T extends { length: 8 } ? `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}${D}${T[4]}${D}${T[5]}${D}${T[6]}${D}${T[7]}`
      : T extends { length: 9 } ? `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}${D}${T[4]}${D}${T[5]}${D}${T[6]}${D}${T[7]}${D}${T[8]}`
      : `${T[0]}${D}${T[1]}${D}${T[2]}${D}${T[3]}${D}${T[4]}${D}${T[5]}${D}${T[6]}${D}${T[7]}${D}${T[8]}${D}${T[9]}`;
    
    type DotTreePath = Join<OtherTreePath>;
    // "nested" | "e" | "nested.props" | "nested.props.a" | "nested.props.b" | "nested.d"
    

    Link to TS playground

        4
  •  4
  •   Chris Sandvik    1 年前

    这是我的方法,我从这篇文章中得到的 TypeScript Utility: keyof nested object 并扭曲它以支持自引用类型 :

    使用TS>4.1(不知道它是否适用于以前的版本)

    type Key = string | number | symbol;
    
    type Join<L extends Key | undefined, R extends Key | undefined> = L extends
      | string
      | number
      ? R extends string | number
        ? `${L}.${R}`
        : L
      : R extends string | number
      ? R
      : undefined;
    
    type Union<
      L extends unknown | undefined,
      R extends unknown | undefined
    > = L extends undefined
      ? R extends undefined
        ? undefined
        : R
      : R extends undefined
      ? L
      : L | R;
    
    // Use this type to define object types you want to skip (no path-scanning)
    type ObjectsToIgnore = { new(...parms: any[]): any } | Date | Array<any>
    
    type ValidObject<T> =  T extends object 
      ? T extends ObjectsToIgnore 
        ? false & 1 
        : T 
      : false & 1;
    
    export type DotPath<
      T extends object,
      Prev extends Key | undefined = undefined,
      Path extends Key | undefined = undefined,
      PrevTypes extends object = T
    > = string &
      {
        [K in keyof T]: 
        // T[K] is a type alredy checked?
        T[K] extends PrevTypes | T
          //  Return all previous paths.
          ? Union<Union<Prev, Path>, Join<Path, K>>
          : // T[K] is an object?.
          Required<T>[K] extends ValidObject<Required<T>[K]>
          ? // Continue extracting
            DotPath<Required<T>[K], Union<Prev, Path>, Join<Path, K>, PrevTypes | T>       
          :  // Return all previous paths, including current key.
          Union<Union<Prev, Path>, Join<Path, K>>;
      }[keyof T];
    

    编辑 :使用此类型的方法如下:

    type MyGenericType<T extends POJO> = {
      keys: DotPath<T>[];
    };
    
    const test: MyGenericType<NestedObjectType> = {
      // If you need it expressed as ["nest", "c"] you can
      // use .split('.'), or perhaps changing the "Join" type.
      keys: ['a', 'nest.c', 'otherNest.c']
    }
    

    重要的 :由于DotPath类型是现在定义的,它不会让你选择任何数组字段的属性,也不会让你在找到自引用类型后选择更深层次的属性。例子:

    type Tree = {
     nodeVal: string;
     parent: Tree;
     other: AnotherObjectType 
    }
    
    type AnotherObjectType = {
       numbers: number[];
       // array of objects
       nestArray: { a: string }[];
       // referencing to itself
       parentObj: AnotherObjectType;
       // object with self-reference
       tree: Tree
     }
    type ValidPaths = DotPath<AnotherObjectType>;
    const validPaths: ValidPaths[] = ["numbers", "nestArray", "parentObj", "tree", "tree.nodeVal", "tree.parent", "tree.obj"];
    const invalidPaths: ValidPaths[] = ["numbers.lenght", "nestArray.a", "parentObj.numbers", "tree.parent.nodeVal", "tree.obj.numbers"]
    

    最后,我会留下一个 Playground (更新版本,案例由czlowiek488和Jerry H提供)

    EDIT2 :对以前版本的一些修复。

    EDIT3 :支持可选字段。

    EDIT4 :允许跳过特定的非基元类型(如日期和数组)

        5
  •  2
  •   James Yuriy Lug    2 年前

    我偶然发现了这个解决方案,它可以处理数组中的嵌套对象属性和可以为null的成员(请参阅此 Gist 更多细节)。

    type Paths<T> = T extends Array<infer U>
      ? `${Paths<U>}`
      : T extends object
      ? {
          [K in keyof T & (string | number)]: K extends string
            ? `${K}` | `${K}.${Paths<T[K]>}`
            : never;
        }[keyof T & (string | number)]
      : never;
    

    其工作原理如下:

    • 它需要一个对象或数组类型 T 作为一个参数。
    • 如果 T 是一个数组,它使用 infer 关键字来推断其元素的类型,并递归应用 Paths 键入他们。
    • 如果 T 是一个对象,它创建了一个新的对象类型,其键与 T ,但每个值都使用字符串文字替换为其路径。
    • 它使用 keyof 操作符获取所有键的联合类型 T 它们是字符串或数字。
    • 它递归地应用 路径 键入剩余值。
    • 它返回所有结果路径的联合类型。

    这个 路径 type可以这样使用:

    interface Package {
      name: string;
      man?: string[];
      bin: { 'my-program': string };
      funding?: { type: string; url: string }[];
      peerDependenciesMeta?: {
        'soy-milk'?: { optional: boolean };
      };
    }
    
    // Create a list of keys in the `Package` interface
    const list: Paths<Package>[] = [
      'name', // OK
      'man', // OK
      'bin.my-program', // OK
      'funding', // OK
      'funding.type', // OK
      'peerDependenciesMeta.soy-milk', // OK
      'peerDependenciesMeta.soy-milk.optional', // OK
      'invalid', // ERROR: Type '"invalid"' is not assignable to type ...
      'bin.other', // ERROR: Type '"other"' is not assignable to type ...
    ];
    
        6
  •  0
  •   itay oded    3 年前

    这可能对你有帮助,兄弟

    https://github.com/react-hook-form/react-hook-form/blob/master/src/types/path/eager.ts#L61

    Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
    

    https://github.com/react-hook-form/react-hook-form/blob/master/src/types/path/eager.ts#L141

    PathValue<{foo: {bar: string}}, 'foo.bar'> = string
    
        7
  •  0
  •   Apperside    2 年前

    我在这篇文章中尝试了公认的答案,它奏效了,但编译器速度非常慢。我认为我找到的黄金标准是 react-hook-form s Path 类型实用程序。我看到@wangzi已经在另一个答案中提到了,但他只是链接到了他们的源文件。我在一个正在进行的项目中需要这个,不幸的是,我们正在使用Formik,所以我的团队不希望我仅仅为这个工具安装RHF。所以我遍历并提取了所有依赖类型utils,这样我就可以独立使用它们了。

    type Primitive = null | undefined | string | number | boolean | symbol | bigint;
    
    type IsEqual<T1, T2> = T1 extends T2
      ? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
        ? true
        : false
      : false;
    
    interface File extends Blob {
      readonly lastModified: number;
      readonly name: string;
    }
    
    interface FileList {
      readonly length: number;
      item(index: number): File | null;
      [index: number]: File;
    }
    
    type BrowserNativeObject = Date | FileList | File;
    
    type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
      ? false
      : true;
    
    type TupleKeys<T extends ReadonlyArray<any>> = Exclude<keyof T, keyof any[]>;
    
    type AnyIsEqual<T1, T2> = T1 extends T2
      ? IsEqual<T1, T2> extends true
        ? true
        : never
      : never;
    
    type PathImpl<K extends string | number, V, TraversedTypes> = V extends
      | Primitive
      | BrowserNativeObject
      ? `${K}`
      : true extends AnyIsEqual<TraversedTypes, V>
      ? `${K}`
      : `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;
    
    type ArrayKey = number;
    
    type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
      ? IsTuple<T> extends true
        ? {
            [K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
          }[TupleKeys<T>]
        : PathImpl<ArrayKey, V, TraversedTypes>
      : {
          [K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
        }[keyof T];
    
    export type Path<T> = T extends any ? PathInternal<T> : never;
    
    

    经过测试,我发现它一碰到自引用类型循环就会停止,我认为这是一种合理的方法。它还支持在任何时候停止 BrowserNativeObject ,在这种情况下,这应该被视为一个基本/停止点。我不能说我完全理解这种类型的工作原理,但我确实知道它表现得很好,这是我在自己的项目中发现的最好的选择。

    Here's a playground demoing it

        8
  •  0
  •   Dromo    2 年前

    Aram Becker对数组和空路径的支持补充道:

    type Vals<T> = T[keyof T];
    type PathsOf<T> =
        T extends object ?
        T extends Array<infer Item> ?
        [] | [number] | [number, ...PathsOf<Item>] :
        Vals<{[P in keyof T]-?: [] | [P] | [P, ...PathsOf<T[P]>]}> :
        [];