TS4.1 更新现在可以使用microsoft/TypeScript#40336 中实现的模板文字类型,在类型级别连接字符串文字。可以调整以下实现以使用它而不是 Cons 之类的东西(它本身可以使用 variadic tuple types 作为 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
PRE-TS4.1 答案:
如前所述,目前无法在类型级别连接字符串文字。有一些建议可能会允许这样做,例如 a suggestion to allow augmenting keys during mapped types 和 a suggestion to validate string literals via regular expression,但目前这是不可能的。
您可以将它们表示为字符串文字的tuples,而不是将路径表示为虚线字符串。所以"a"变成["a"],"nest.c"变成["nest", "c"]。在运行时,很容易通过split() 和join() 方法在这些类型之间进行转换。
所以你可能想要像Paths<T> 这样的东西,它返回给定类型T 的所有路径的联合,或者可能是Leaves<T>,它只是Paths<T> 中指向非对象类型本身的那些元素.没有对这种类型的内置支持; ts-toolbelt 库 has this,但由于我无法在 Playground 中使用该库,所以我将在此处推出自己的库。
请注意:Paths 和 Leaves 本质上是递归的,这对编译器来说可能非常繁重。 recursive types of the sort needed for this 在 TypeScript 中也是 not officially supported。我将在下面展示的是以这种不确定/不真正支持的方式递归的,但我尝试为您提供一种指定最大递归深度的方法。
我们开始吧:
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 类型是一个长元组,您可以使用它来获取前一个数字(最大为最大值)。所以Prev[10] 是9,Prev[1] 是0。当我们深入对象树时,我们需要它来限制递归。
最后,Paths<T, D> 和 Leaves<T, D> 是通过进入每个对象类型 T 并收集键,然后 Cons 将它们添加到这些键的属性的 Paths 和 Leaves 上来实现的。它们之间的区别在于Paths 还直接包含联合中的子路径。默认情况下,深度参数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-toolbelt 或其他一些实现可能会更加小心,以免导致编译器心脏病发作。所以我不一定说你应该在没有大量测试的情况下在你的生产代码中使用它。
但无论如何,这是你想要的类型,假设你有并且想要Paths:
type MyGenericType<T extends object> = {
keys: Array<Paths<T>>;
};
const test: MyGenericType<NestedObjectType> = {
keys: [['a'], ['nest', 'c']]
}
希望有所帮助;祝你好运!
Link to code