提问人:CalibanAngel 提问时间:10/17/2019 更新时间:9/19/2023 访问量:65913
Typescript:嵌套对象的深键
Typescript: deep keyof of a nested object
问:
所以我想找到一种方法来拥有嵌套对象的所有键。
我有一个泛型类型,它采用参数中的类型。我的目标是获取给定类型的所有密钥。
在这种情况下,以下代码效果很好。但是当我开始使用嵌套对象时,情况就不同了。
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
答:
目前,最简单的方法可以做到这一点,而不必担心边缘情况
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"
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;
这里用中间的点连接两个字符串,除非最后一个字符串是空的。所以是而是.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"]
}
其余的答案基本相同。TS4.1 中也支持递归条件类型(如 microsoft/TypeScript#40002 中实现的那样),但递归限制仍然适用,因此如果没有深度限制器(如 .Prev
请注意,这将使非点键成为虚线路径,例如可能产生 .因此,请注意避免这样的键,或重写上述内容以排除它们。{foo: [{"bar-baz": 1}]}
foo.0.bar-baz
另请注意:这些递归类型本质上是“棘手的”,如果稍作修改,往往会让编译器不满意。如果你不走运,你会看到像“类型实例化太深”这样的错误,如果你非常不走运,你会看到编译器吃掉你所有的CPU,永远无法完成类型检查。我不确定该怎么说这种问题......只是这样的事情有时比它们的价值更麻烦。
TS4.1 之前的答案:
如前所述,目前无法在类型级别连接字符串文本。有一些建议可能允许这样做,例如允许在映射类型期间增加键的建议,以及通过正则表达式验证字符串文字的建议,但目前这是不可能的。
您可以将它们表示为字符串文本的元组,而不是将路径表示为虚线字符串。所以变成 ,变成 。在运行时,通过 和 方法在这些类型之间进行转换非常容易。"a"
["a"]
"nest.c"
["nest", "c"]
split()
join()
因此,您可能想要类似的东西,返回给定类型的所有路径的联合,或者可能只是那些指向非对象类型本身的元素。对此类类型没有内置支持;ts-toolbelt 库有这个,但由于我不能在 Playground 中使用那个库,所以我会在这里滚动我自己的库。Paths<T>
T
Leaves<T>
Paths<T>
请注意:并且本质上是递归的,可能会给编译器带来很大的负担。TypeScript 中也不正式支持为此所需的递归类型。我将在下面介绍的是以这种不稳定/不真正支持的方式递归的,但我试图为您提供一种指定最大递归深度的方法。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]
: [];
其目的是采用任何类型和元组类型,并生成一个预置在 上的新元组。所以应该是.该实现使用 rest/spread 元组。我们需要它来建立路径。Cons<H, T>
H
T
H
T
Cons<1, [2,3,4]>
[1,2,3,4]
该类型是一个长元组,可用于获取上一个数字(最大值)。所以是 ,也是 。当我们深入对象树时,我们将需要它来限制递归。Prev
Prev[10]
9
Prev[1]
0
最后,通过向下遍历每个对象类型并收集键,并将它们连接到这些键处的属性和来实现。它们之间的区别在于,它还直接包括联合中的子路径。默认情况下,深度参数是 ,每下降一步,我们就会减少 1,直到我们尝试超过 ,此时我们停止递归。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']]
}
评论
Type instantiation is excessively deep and possibly infinite
Paths
infer
Join<K, Paths<T[K], Prev[D]>>
(Paths<T[K], Prev[D]> extends infer R ? Join<K, R> : never)
Prev
export type Prev = [never, 0, 1, 2, 3, 4, ...0[]];
Prev
Cons
Cons<H, T>
[H, ...T]
我遇到了一个类似的问题,当然,上面的答案非常惊人。但对我来说,它有点过头了,如前所述,这对编译器来说是相当费力的。
虽然没有那么优雅,但更易于阅读,我建议使用以下类型来生成类似 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"
评论
Path
PathTree
Path
PathTree
-?
-?
[keyof T]
keyof PathTree<T>
[keyof T]
-?
TS
...any[]
undefined
-?
因此,上述解决方案确实有效,但是,它们要么语法有些混乱,要么给编译器带来了很大的压力。下面是一个编程建议,适用于您只需要一个字符串的用例:
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 表示路径当前指向的字段的类型
一个递归类型函数,使用条件类型、模板文本字符串、映射类型和索引访问类型,基于@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"]
这在使用 MongoDB 或 Firebase 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>
您还可以更新 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;
评论
typeof objectName
NestedObjectKeys
Partial<{ [key in KeyPath<T>]: any & T}> & string
"PREFIX." + dynamicKey
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]>]}> :
[];
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>, ".">;
评论
这是我的解决方案:)
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];
这是我的解决方案。支持 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 上查看代码和示例
评论
我在寻找一种强类型化对象路径的方法时遇到了这个问题。 我发现 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] : ""
这是我的方法,我从这篇文章中获取了它 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:允许跳过特定的非原始类型(如日期和数组)
评论
这是我的解决方案。我找到的最短的方法。同样在这里,我有一个数组检查
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]
这可能会对你有所帮助,兄弟
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
评论
我遇到了这个解决方案,它适用于数组和可为 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
- 它以递归方式将类型应用于其余值。
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 ...
];
我在这篇文章中尝试了公认的答案,它起作用了,但编译器的速度很慢。我认为我为此找到的黄金标准是 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
评论
c
nest
otherNest
{keys: [["a"], ["nest", "c"]]}
paths
keys
Paths<NestedObjectType>
[] | ["a"] | ["b"] | ["nest"] | ["nest", "c"] | ["otherNest"] | ["otherNest", "c"]
["a"] | ["b"] | ["nest", "c"] | ["otherNest", "c"]
Paths
Paths<Tree>
type Tree = {l: Tree, r: Tree}
Paths