打字稿:嵌套对象的深度键

Cal*_*gel 31 typescript

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

我有一个在参数中采用类型的泛型类型。我的目标是获取给定类型的所有键。

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

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'];
}
Run Code Online (Sandbox Code Playgroud)

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

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'];
}
Run Code Online (Sandbox Code Playgroud)

那么在不使用函数的情况下,我该怎么做才能将这种键提供给test

jca*_*alz 83

TS4.1 更新 现在可以使用microsoft/TypeScript#40336 中实现的模板文字类型在类型级别连接字符串文字。下面的实现可以进行调整,而不是使用这样的事情(这本身可以采用以下方式实现可变参数的元组类型在打字稿4.0中引入的):Cons

type Join<K, P> = K extends string | number ?
    P extends string | number ?
    `${K}${"" extends P ? "" : "."}${P}`
    : never : never;
Run Code Online (Sandbox Code Playgroud)

这里Join用中间的点连接两个字符串,除非最后一个字符串为空。所以Join<"a","b.c">"a.b.c"同时Join<"a","">"a"

然后PathsLeaves成为:

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] : "";
Run Code Online (Sandbox Code Playgroud)

其他类型不属于它:

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"
Run Code Online (Sandbox Code Playgroud)

type MyGenericType<T extends object> = {
    keys: Array<Paths<T>>;
};

const test: MyGenericType<NestedObjectType> = {
    keys: ["a", "nest.c"]
}
Run Code Online (Sandbox Code Playgroud)

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

请注意,这将使非点式键的点路径,例如{foo: [{"bar-baz": 1}]}可能会产生foo.0.bar-baz. 所以要小心避免这样的键,或者重写上面的以排除它们。

另请注意:这些递归类型本质上是“棘手的”,如果稍作修改,往往会使编译器不满意。如果你不走运,你会看到诸如“类型实例化太深”之类的错误,如果你很不走运,你会看到编译器耗尽了你所有的 CPU 并且永远不会完成类型检查。总的来说,我不知道对这类问题该说什么……只是这些事情有时比它们的价值更麻烦。

Playground 链接到代码



TS4.1 之前的答案:

如前所述,目前无法在类型级别连接字符串文字。有一些建议可能允许这样做,例如允许在映射类型期间增加键的建议通过正则表达式验证字符串文字的建议,但目前这是不可能的。

您可以将路径表示为字符串文字的元组,而不是将路径表示为虚线字符串。所以"a"成为["a"],并且"nest.c"成为["nest", "c"]。在运行时很容易通过split()join()方法在这些类型之间进行转换。


所以你可能想要类似的东西Paths<T>返回给定 type 的所有路径的联合T,或者可能Leaves<T>只是那些Paths<T>指向非对象类型本身的元素。没有对这种类型的内置支持;在TS-工具区图书馆有这个,但因为我不能在使用该库游乐场,我将在这里推出自己的。

请注意:Paths并且Leaves本质上是递归的,这可能会给编译器带来很大的负担。和递归类型需要这个排序不正式支持在打字稿无论是。我将在下面介绍的是以这种不确定/并非真正支持的方式递归的,但我尝试为您提供一种指定最大递归深度的方法。

开始了:

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]
    : [];
Run Code Online (Sandbox Code Playgroud)

的目的Cons<H, T>是采用任何类型H和元组类型T并生成一个新的元组,并HT. 所以Cons<1, [2,3,4]>应该是[1,2,3,4]。该实现使用rest/spread tuples。我们需要这个来建立路径。

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

最后,Paths<T, D>Leaves<T, D>通过走到每个对象类型实现T和收集键,和Cons荷兰国际集团它们放到PathsLeaves在这些密钥的属性。它们之间的区别在于Paths还直接包括联合中的子路径。默认情况下,深度参数D10,每向下一步,我们都会减少D1,直到我们尝试越过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"]
Run Code Online (Sandbox Code Playgroud)

为了查看深度限制的用处,假设我们有一个这样的树类型:

interface Tree {
    left: Tree,
    right: Tree,
    data: string
}
Run Code Online (Sandbox Code Playgroud)

嗯,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 ... | [...]
Run Code Online (Sandbox Code Playgroud)

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

type TreeLeaves = Leaves<Tree, 3>;
// type TreeLeaves2 = ["data"] | ["left", "data"] | ["right", "data"] |
// ["left", "left", "data"] | ["left", "right", "data"] | 
// ["right", "left", "data"] | ["right", "right", "data"]
Run Code Online (Sandbox Code Playgroud)

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


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

但无论如何,这是您想要的类型,假设您拥有并想要Paths

type MyGenericType<T extends object> = {
    keys: Array<Paths<T>>;
};

const test: MyGenericType<NestedObjectType> = {
    keys: [['a'], ['nest', 'c']]
}
Run Code Online (Sandbox Code Playgroud)

希望有所帮助;祝你好运!

代码链接

  • 有时使用“Paths”时(即使深度较低为 3),我收到“类型实例化过深且可能无限”的信息。对我有用的解决方法是使用“infer”,将“Join&lt;K, Paths&lt;T[K], Prev[D]&gt;&gt;”替换为“(Paths&lt;T[K], Prev[D]&gt; extends推断 R ?加入&lt;K, R&gt; :从不)`。 (6认同)
  • @MartinŽdila 和@Qtax,我也遇到了这个问题。如果您不需要深入 20 个对象,则可以缩短“Prev”的范围。对于我的项目,我知道它最多只能达到 2 个对象深度,所以我将我的项目缩短为这样: `export type Prev = [never, 0, 1, 2, 3, 4, ... 0[]];`。小编辑:他谈到了“Prev”的目的,他说“答案的其余部分基本上是相同的”。 (3认同)
  • 无论如何,如果我确实提供了反向类型,那么它必须在一个新问题中,因为注释部分不是呈现明显不同的代码和解释的好地方。 (2认同)
  • “... Cons(它本身可以使用 TypeScript 4.0 中引入的可变元组类型来实现)” - 对于任何想知道如何执行此操作的人:使用 TS 4.0+,您可以删除实用程序类型 `Cons` 并替换任何 ` Cons&lt;H, T&gt;` 通过 `[H, ...T]` (2认同)

min*_*lid 39

基于@jcalz 的答案,使用条件类型模板文字字符串、映射类型索引访问类型的递归类型函数,可以使用此ts Playground 示例进行验证

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

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"]
Run Code Online (Sandbox Code Playgroud)

当使用mongodbfirebase firestore等文档数据库时,这也很有用,可以使用点表示法设置单个嵌套属性

使用mongodb

db.collection("products").update(
   { _id: 100 },
   { $set: { "details.make": "zzz" } }
)
Run Code Online (Sandbox Code Playgroud)

带火力底座

db.collection("users").doc("frank").update({
   "age": 13,
   "favorites.color": "Red"
})
Run Code Online (Sandbox Code Playgroud)

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

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

export type DocumentUpdate<T> = Partial<{ [key in DotNestedKeys<T>]: any & T}> & Partial<T>
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

您还可以更新 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;
Run Code Online (Sandbox Code Playgroud)


Ara*_*ker 8

我遇到了类似的问题,当然,上面的答案非常惊人。但对我来说,它有点过头了,正如前面提到的那样对编译器来说是相当繁重的。

虽然不那么优雅,但更易于阅读,我建议使用以下类型来生成类似路径的元组:

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

type Path<T> = PathTree<T>[keyof PathTree<T>];
Run Code Online (Sandbox Code Playgroud)

一个主要缺点是,这种类型无法处理自引用类型,例如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)
Run Code Online (Sandbox Code Playgroud)

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

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"]
Run Code Online (Sandbox Code Playgroud)

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

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

type OtherPath = Path<OtherTree>;
// ["nested", "props", "a"] | ["nested", "props", "b"] | ["nested", "d"] | ["e"]
Run Code Online (Sandbox Code Playgroud)

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


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

// 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"
Run Code Online (Sandbox Code Playgroud)

链接到 TS 游乐场

  • 这个解决方案很慢,因为它遭受类型爆炸。每次使用 `Path` 时,它都会调用 `PathTree` 两次,每个键调用一次 `Path`,再调用 `PathTree` 两次,等等......此外,不需要 `-?`。通过删除“-?”并使用“[keyof T]”而不是“keyof PathTree&lt;T&gt;”,我们可以通过避免类型爆炸以更有效的方式获得相同的结果。 (2认同)
  • @TheMrZZ 你对“[keyof T]”的看法是绝对正确的,第二个调用是非常无用的。`-?` 的作用是防止 `TS` 默认为 `...any[]`。当涉及可选属性时,似乎会发生这种情况。当删除游乐场链接中的“-?”时,您可以看到该行为。 (2认同)

Joa*_*let 7

这是我的方法,我从这篇文章 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];
Run Code Online (Sandbox Code Playgroud)

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

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']
}
Run Code Online (Sandbox Code Playgroud)

重要提示:由于现在定义了 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"]
Run Code Online (Sandbox Code Playgroud)

最后,我将离开一个游乐场 (更新版本,案例由czlowiek488和Jerry H提供)

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

EDIT3:支持可选字段。

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


小智 7

我遇到了这个解决方案,它适用于数组和可为空成员内的嵌套对象属性(有关更多详细信息,请参阅此要点)。

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;
Run Code Online (Sandbox Code Playgroud)

它的工作原理如下:

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

Paths类型可以这样使用:

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 ...
];
Run Code Online (Sandbox Code Playgroud)