Typescript:嵌套对象的深键

Typescript: deep keyof of a nested object

提问人:CalibanAngel 提问时间:10/17/2019 更新时间:9/19/2023 访问量:65913

问:

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

我有一个泛型类型,它采用参数中的类型。我的目标是获取给定类型的所有密钥。

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

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

打字稿

评论

1赞 Juraj Kocan 10/17/2019
没有办法像你想要的“nest.c”那样限制 strig 文字,但如果这样做,你可以在你的键中包含“c”
0赞 CalibanAngel 10/17/2019
@JurajKocan 好吧,正如你所看到的,它同时存在于 和 中。所以我不认为这是enougth。您的解决方案是什么?cnestotherNest
1赞 jcalz 10/17/2019
您可以将路径表示为元组,例如(在这里我可能会说而不是 )。如果是(或者如果您只关心叶节点),那会起作用吗?即便如此,这有点棘手,因为明显的实现是递归的,并不完全支持。另外,你想成为什么样的人?{keys: [["a"], ["nest", "c"]]}pathskeysPaths<NestedObjectType>[] | ["a"] | ["b"] | ["nest"] | ["nest", "c"] | ["otherNest"] | ["otherNest", "c"]["a"] | ["b"] | ["nest", "c"] | ["otherNest", "c"]PathsPaths<Tree>type Tree = {l: Tree, r: Tree}
0赞 CalibanAngel 10/17/2019
@jcalz我认为它会完美地工作。你知道是否已经有原生的内置或库实现了吗?Paths
0赞 Paleo 10/17/2019
也许相关:stackoverflow.com/questions/58361316/......

答:

229赞 jcalz 10/18/2019 #1

目前,最简单的方法可以做到这一点,而不必担心边缘情况

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 代码链接


TS4.1 更新现在可以使用 microsoft/TypeScript#40336 中实现的模板文本类型在类型级别连接字符串文本。下面的实现可以调整为使用它而不是类似的东西(它本身可以使用 TypeScript 4.0 中引入的可变元组类型来实现):Cons

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

这里用中间的点连接两个字符串,除非最后一个字符串是空的。所以是而是.JoinJoin<"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] : "";

而其他类型则不属于它:

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"]
}

其余的答案基本相同。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()


因此,您可能想要类似的东西,返回给定类型的所有路径的联合,或者可能只是那些指向非对象类型本身的元素。对此类类型没有内置支持;ts-toolbelt有这个,但由于我不能在 Playground 中使用那个库,所以我会在这里滚动我自己的库。Paths<T>TLeaves<T>Paths<T>

请注意:并且本质上是递归的,可能会给编译器带来很大的负担。TypeScript 中也不正式支持为此所需的递归类型。我将在下面介绍的是以这种不稳定/不真正支持的方式递归的,但我试图为您提供一种指定最大递归深度的方法。PathsLeaves

来吧:

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]
    : [];

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

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

最后,通过向下遍历每个对象类型并收集键,并将它们连接到这些键处的属性和来实现。它们之间的区别在于,它还直接包括联合中的子路径。默认情况下,深度参数是 ,每下降一步,我们就会减少 1,直到我们尝试超过 ,此时我们停止递归。Paths<T, D>Leaves<T, D>TConsPathsLeavesPathsD10D0


好的,让我们来测试一下:

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

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

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

const test: MyGenericType<NestedObjectType> = {
    keys: [['a'], ['nest', 'c']]
}

代码链接

评论

2赞 jcalz 5/6/2020
无论如何,如果我确实提供了一个反向类型,它必须在一个新问题中,因为评论部分不是展示明显不同的代码和解释的好地方。
1赞 jeben 5/7/2020
@jcalz谢谢你的回答,我实际上试过了,这是我的问题:stackoverflow.com/questions/61644053/......
6赞 Qtax 9/23/2020
我在使用某些时候得到了(即使深度低至 3)。一个对我有用的解决方法是使用 ,将 替换为 .Type instantiation is excessively deep and possibly infinitePathsinferJoin<K, Paths<T[K], Prev[D]>>(Paths<T[K], Prev[D]> extends infer R ? Join<K, R> : never)
3赞 Michael Ziluck 1/30/2021
@MartinŽdila@Qtax,我也遇到了这个问题。如果不需要深入 20 个对象,则可以缩短 的范围。对于我的项目,我知道它最多只能达到 2 个对象的深度,所以我将我的缩短为这样:.小编辑:他谈到了权利的目的,他说“其余的答案基本相同”。Prevexport type Prev = [never, 0, 1, 2, 3, 4, ...0[]];Prev
2赞 YetAnotherFrank 12/7/2022
"...缺点(本身可以使用 TypeScript 4.0 中引入的可变元组类型来实现)“ - 对于任何想知道如何做到这一点的人:使用 TS 4.0+,您可以删除实用程序类型并替换任何ConsCons<H, T>[H, ...T]
16赞 Aram Becker 1/30/2021 #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];

一个主要的缺点是,这种类型不能处理自引用类型,比如 from @jcalz answer:Tree

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 的答案的点语法时,您可以将元组映射到模板字符串类型:

// 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"

链接到 TS 操场

评论

0赞 christo8989 3/23/2022
对于生产环境来说,这似乎仍然太慢了。:(
1赞 Nader Zouaoui 6/3/2022
@christo8989 为什么要在生产环境中使用 Typescript?
1赞 Aram Becker 6/7/2022
我认为他们的意思是“生产环境”是指在生产环境中运行的大规模、积极使用的项目。
2赞 TheMrZZ 6/10/2022
这种解决方案很慢,因为它会受到类型爆炸的影响。每次使用,它调用两次,每个键调用一次,调用两次,以此类推......此外,不需要 .通过删除和使用 代替 ,我们通过避免类型爆炸以更有效的方式获得相同的结果。PathPathTreePathPathTree-?-?[keyof T]keyof PathTree<T>
2赞 Aram Becker 6/11/2022
@TheMrZZ 你说的绝对正确,第二个电话是没用的。存在是为了防止默认为 .当涉及可选属性时,这似乎会发生这种情况。您可以在 Playground 链接中移除时看到该行为。[keyof T]-?TS...any[]undefined-?
3赞 nosknut 6/21/2021 #3

因此,上述解决方案确实有效,但是,它们要么语法有些混乱,要么给编译器带来了很大的压力。下面是一个编程建议,适用于您只需要一个字符串的用例:

type PathSelector<T, C = T> = (C extends {} ? {
    [P in keyof C]: PathSelector<T, C[P]>
} : C) & {
    getPath(): string
}

function pathSelector<T, C = T>(path?: string): PathSelector<T, C> {
    return new Proxy({
        getPath() {
            return path
        },
    } as any, {
        get(target, name: string) {
            if (name === 'getPath') {
                return target[name]
            }
            return pathSelector(path === undefined ? name : `${path}.${name}` as any)
        }
    })
}

type SomeObject = {
    value: string
    otherValue: number
    child: SomeObject
    otherChild: SomeObject
}
const path = pathSelector<SomeObject>().child.child.otherChild.child.child.otherValue
console.log(path.getPath())// will print: "child.child.otherChild.child.child.otherValue"
function doSomething<T, K>(path: PathSelector<T, K>, value: K){
}
// since otherValue is a number:
doSomething(path, 1) // works
doSomething(path, '1') // Error: Argument of type 'string' is not assignable to parameter of type 'number'

类型参数 T 将始终保持与原始请求对象相同的类型,以便可用于验证路径是否确实来自指定对象。

C 表示路径当前指向的字段的类型

37赞 mindlid 7/16/2021 #4

一个递归类型函数,使用条件类型、模板文本字符串、映射类型和索引访问类型,基于@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"]

这在使用 MongoDBFirebase Firestore 等文档数据库时也很有用,这些数据库允许使用点表示法设置单个嵌套属性

使用 mongodb

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

带火力基地

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;

评论

0赞 DoneDeal0 8/19/2021
嗨,很棒的解决方案!有没有办法只显示直接子节点而不是所有可能的路径?例如:a、b、nest、otherNest,而不是 nest、nest.c 等。因此,只有当用户键入“nest”时,nest.c 才会出现。
0赞 mindlid 8/20/2021
@DoneDeal0是的,在这种情况下,您无需执行任何操作,只需传递原始类型即可。
0赞 DoneDeal0 8/20/2021
我尝试用 键入函数的参数,但它不起作用。也许我做错了什么?codesandbox.io/s/aged-darkness-s6kmx?file=/src/App.tsxtypeof objectName
0赞 DoneDeal0 8/27/2021
最后一个问题,是否可以在保持自动完成的同时允许类型中的未知字符串?如果我用 - 如您的示例中建议的那样 - 自动完成不再可用。但是,如果没有这种额外的类型化,打字稿将拒绝任何与原始对象结构不匹配的字符串。因此,它与动态类型不兼容(例如:将返回错误)。我用你的代码做了一个沙盒:codesandbox.io/s/modest-robinson-8b1cj?file=/src/App.tsx .NestedObjectKeysPartial<{ [key in KeyPath<T>]: any & T}> & string"PREFIX." + dynamicKey
1赞 Gisheri 4/20/2022
如果对象具有循环引用,则此操作将失败。我想知道是否有可能限制它允许的深度
3赞 Luke Miles 11/2/2021 #5

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]>]}> :
    [];
2赞 Omer Prizner 12/6/2021 #6
import { List } from "ts-toolbelt";
import { Paths } from "ts-toolbelt/out/Object/Paths";

type Join<T extends List.List, D extends string> = T extends []
  ? ""
  : T extends [(string | number | boolean)?]
  ? `${T[0]}`
  : T extends [(string | number | boolean)?, ...infer U]
  ? `${T[0]}` | `${T[0]}${D}${Join<U, D>}`
  : never;

export type DottedPaths<V> = Join<Paths<V>, ".">;

评论

0赞 m0onspell 12/11/2021
不适用于自引用类型
0赞 Omer Prizner 12/23/2021
@m0onspell 添加了一个更干净的版本。希望这会有所帮助。
0赞 itay oded 2/28/2022 #7

这是我的解决方案:)

type Primitive = string | number | boolean;

type JoinNestedKey<P, K> = P extends string | number ? `${P}.${K extends string | number ? K : ''}` : K;

export type NestedKey<T extends Obj, P = false> = {
  [K in keyof T]: T[K] extends Primitive ? JoinNestedKey<P, K> : JoinNestedKey<P, K> | NestedKey<T[K], JoinNestedKey<P, K>>;
}[keyof T];
2赞 Yuriy Lug 4/6/2022 #8

这是我的解决方案。支持 dtos、文字类型、非必需键、数组和相同的嵌套。使用名为 GetDTOKeys 的类型

type DTO = Record<string, any>;
type LiteralType = string | number | boolean | bigint;
type GetDirtyDTOKeys<O extends DTO> = {
  [K in keyof O]-?: NonNullable<O[K]> extends Array<infer A>
    ? NonNullable<A> extends LiteralType
      ? K
      : K extends string
        ? GetDirtyDTOKeys<NonNullable<A>> extends infer NK
          ? NK extends string
            ? `${K}.${NK}`
            : never
          : never
        : never
    : NonNullable<O[K]> extends LiteralType
      ? K
      : K extends string
        ? GetDirtyDTOKeys<NonNullable<O[K]>> extends infer NK
          ? NK extends string
            ? `${K}.${NK}`
            : never
          : never
        : never
}[keyof O];
type AllDTOKeys = string | number | symbol;
type TrashDTOKeys = `${string}.undefined` | number | symbol;
type ExcludeTrashDTOKeys<O extends AllDTOKeys> = O extends TrashDTOKeys ? never : O;
type GetDTOKeys<O extends DTO> = ExcludeTrashDTOKeys<GetDirtyDTOKeys<O>>;

您可以在 playground 上查看代码和示例

评论

0赞 James 3/15/2023
其他答案不允许访问数组中包含的对象的嵌套属性,而这个答案允许。
0赞 Apperside 4/29/2022 #9

我在寻找一种强类型化对象路径的方法时遇到了这个问题。 我发现 Michael Ziluck 的答案是最优雅、最完整的,但它缺少我需要的东西:处理数组属性。 我需要的是一些东西,给定这个示例结构:

type TypeA = {
    fieldA1: string
    fieldA2:
}

type TypeB = {
    fieldB1: string
    fieldB2: string
}

type MyType = {
    field1: string
    field2: TypeA,
    field3: TypeB[]
}

将允许我声明一个接受以下值的类型:

"field1" | "field2" | "field2.fieldA1" | "field2.fieldA2" | "field3" | "field3.fieldB1" | "field3.fieldB2"

不管 field3 是一个数组。

我能够通过更改类型来获得它,如下所示:Paths

export type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]?:
        T[K] extends Array<infer U> ? `${K}` | Join<K, Paths<U, Prev[D]>> :
        K extends string | number ?
        `${K}` | Join<K, Paths<T[K], Prev[D]>>
        : never
    }[keyof T] : ""
7赞 Joaquín Michelet 6/11/2022 #10

这是我的方法,我从这篇文章中获取了它 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:允许跳过特定的非原始类型(如日期和数组)

评论

0赞 lepsch 6/16/2022
您能否通过如何使用这种方法的示例来编辑和改进答案?
0赞 Joaquín Michelet 6/16/2022
@lepsch好的,谢谢你的提问。我也会留下一个操场来测试它。
0赞 czlowiek488 7/17/2022
它几乎可以工作,但是有一种情况它不起作用。如果你有一个像这个接口这样的对象 AnotherObjectType {a: { b: { c: string; zzz: AnotherObjectType } }} 它只会显示 'a','a.b',有人想修复它吗?
0赞 Joaquín Michelet 7/25/2022
嘿,@czlowiek488不错的收获,我想我在遇到同样的问题后得到了它,但我忘了更新这篇文章,我还发现“K extends DotPath<PrevType>”条件来检测自引用是错误的,因为它只检查了道具名称,所以如果一个属性的命名与树中的前一个属性完全相同,它将切断路径。看看编辑是否适合你!
1赞 Joaquín Michelet 11/24/2022
@KevinBeal 已修复,问题是因为它询问值是否从“object”扩展,这对于 Date 类型(以及其他类型)是正确的。我添加了一种配置您不想考虑的其他复杂类型的方法
0赞 Dromo 1/25/2023 #11

这是我的解决方案。我找到的最短的方法。同样在这里,我有一个数组检查

type ObjectPath<T extends object, D extends string = ''> = {
    [K in keyof T]: `${D}${Exclude<K, symbol>}${'' | (T[K] extends object ? ObjectPath<T[K], '.'> : '')}`
}[keyof T]

游乐场链接

4赞 wangzi 2/12/2023 #12

这可能会对你有所帮助,兄弟

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

评论

1赞 BryceLarkin 9/24/2023
就这样吧。请参阅 stackoverflow.com/a/76732921/8400466 以实现
6赞 Albert Mañosa 4/29/2023 #13

我遇到了这个解决方案,它适用于数组和可为 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
  • 如果是一个数组,它使用关键字来推断其元素的类型,并以递归方式将类型应用于它们。TinferPaths
  • 如果 是对象,则它创建一个新的对象类型,其键与 相同,但每个值都使用字符串文本替换为其路径。TT
  • 它使用运算符来获取所有键的联合类型,这些键是字符串或数字。keyofT
  • 它以递归方式将类型应用于其余值。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 ...
];
4赞 Chris Sandvik 7/21/2023 #14

我在这篇文章中尝试了公认的答案,它起作用了,但编译器的速度很慢。我认为我为此找到的黄金标准是 react-hook-form 的类型实用程序。我看到@wangzi已经在单独的答案中提到了它,但他只是链接到他们的源文件。我在我正在做的一个项目中需要它,而且我们(不幸的是)正在使用 Formik,所以我的团队不希望我只为这个实用程序安装 RHF。因此,我浏览并提取了所有依赖类型实用程序,以便我可以独立使用它们。Path

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

这里有一个演示它的游乐场