扁平化界面
Flatten an interface
我有一个用例,我希望能够根据泛型类型派生出我的类型的 keys/fields。
例如:
这些是容器接口
export interface IUser {
id: BigInt;
name: string;
balance: number;
address: Address;
}
export interface Address {
street: string;
city: string;
zipcode: number;
tags: string[];
}
我希望能够定义类似
的类型
const matcher: Matcher<IUser> = {
id: 1
}
这是我可以使用如下实施的 Matcher 来完成的事情
export type Matcher<T> = {
[K in keyof T]: (number | string | boolean)
}
但是现在对于像
这样的用例
const deepMatcher: Matcher<IUser> = {
id: 1,
user.address.city: 'San Francisco'
}
如何更新我的模型 (Matcher) 以支持此用例?
免责声明:这些类型的问题总是有很多边缘情况。此解决方案适用于您的示例,但当我们引入可选属性或联合等内容时可能会中断。此解决方案也不包括数组内对象的路径。
解决方案由两大部分组成:
首先我们需要一个类型,它将采用类型 T
并构造 T
.
的所有路径的并集
type AllPaths<T, P extends string = ""> = {
[K in keyof T]: T[K] extends object
? T[K] extends any[]
? `${P}${K & string}`
: AllPaths<T[K], `${P}${K & string}.`> extends infer O
? `${O & string}` | `${P}${K & string}`
: never
: `${P}${K & string}`
}[keyof T]
AllPaths
是递归类型。每条路径的进度存储在 P
.
中
它映射 T
的所有属性并进行一些检查。如果 T[K]
是一个数组或者不是一个对象,它只是将 K
附加到当前路径并 return 添加它。
T[K] extends object
? T[K] extends any[]
? `${P}${K & string}`
: /* ... */
: `${P}${K & string}`
如果T[K]
是一个对象,我们可以递归调用AllPaths
,将T[K]
作为新的T
和当前路径,再次使用当前key。这次我们还将一个 "."
添加到路径中,因为我们知道稍后将添加一个嵌套的 属性。
AllPaths<T[K], `${P}${K & string}.`> extends infer O
? `${O & string}` | `${P}${K & string}`
: never
这里的 infer O
是一种解决方法,因此 TypeScript 不会抱怨 Type instantiation is excessively deep and possibly infinite
。我们 return 递归调用的结果和当前路径都在这里,所以我们可以将 address
和 address.street
...作为稍后的键。
让我们在这里查看结果:
type T0 = AllPaths<IUser>
// type T0 = "id" | "name" | "balance" | "address" | "address.street" | "address.city" | "address.zipcode" | "address.tags"
我们现在需要一个类型,它接受一个路径并为我们获取正确的路径类型。
type PathToType<T, P extends string> = P extends keyof T
? T[P]
: P extends `${infer L}.${infer R}`
? L extends keyof T
? PathToType<T[L], R>
: never
: never
type T0 = PathToType<IUser, "address.street">
// type T0 = string
这也是递归类型。 P
这次从整条路径开始。
我们首先检查 P
是否是 keyof T
。如果是,我们可以return T[P]
的类型。如果不是,我们尝试将 P
拆分为两个文字类型 L
和 R
,它们必须用点分隔。如果左边的字符串文字匹配 T
的键,我们可以用 T[L]
和路径的其余部分递归调用 PathToType
。
最终我们在Matcher
类型中使用了两种类型。
type Matcher<T> = Partial<{
[K in AllPaths<T>]: PathToType<T, K & string>
}>
我们允许AllPaths<T>
中的任何字符串作为键,并使用PathToType<T, K & string>
获取相应类型的路径。
结果如下所示:
type T0 = Matcher<IUser>
// type T0 = {
// id?: number | undefined;
// name?: string | undefined;
// balance?: number | undefined;
// address?: Address | undefined;
// "address.street"?: string | undefined;
// "address.city"?: string | undefined;
// "address.zipcode"?: number | undefined;
// "address.tags"?: string[] | undefined;
//}
我有一个用例,我希望能够根据泛型类型派生出我的类型的 keys/fields。
例如:
这些是容器接口
export interface IUser {
id: BigInt;
name: string;
balance: number;
address: Address;
}
export interface Address {
street: string;
city: string;
zipcode: number;
tags: string[];
}
我希望能够定义类似
的类型const matcher: Matcher<IUser> = {
id: 1
}
这是我可以使用如下实施的 Matcher 来完成的事情
export type Matcher<T> = {
[K in keyof T]: (number | string | boolean)
}
但是现在对于像
这样的用例const deepMatcher: Matcher<IUser> = {
id: 1,
user.address.city: 'San Francisco'
}
如何更新我的模型 (Matcher) 以支持此用例?
免责声明:这些类型的问题总是有很多边缘情况。此解决方案适用于您的示例,但当我们引入可选属性或联合等内容时可能会中断。此解决方案也不包括数组内对象的路径。
解决方案由两大部分组成:
首先我们需要一个类型,它将采用类型 T
并构造 T
.
type AllPaths<T, P extends string = ""> = {
[K in keyof T]: T[K] extends object
? T[K] extends any[]
? `${P}${K & string}`
: AllPaths<T[K], `${P}${K & string}.`> extends infer O
? `${O & string}` | `${P}${K & string}`
: never
: `${P}${K & string}`
}[keyof T]
AllPaths
是递归类型。每条路径的进度存储在 P
.
它映射 T
的所有属性并进行一些检查。如果 T[K]
是一个数组或者不是一个对象,它只是将 K
附加到当前路径并 return 添加它。
T[K] extends object
? T[K] extends any[]
? `${P}${K & string}`
: /* ... */
: `${P}${K & string}`
如果T[K]
是一个对象,我们可以递归调用AllPaths
,将T[K]
作为新的T
和当前路径,再次使用当前key。这次我们还将一个 "."
添加到路径中,因为我们知道稍后将添加一个嵌套的 属性。
AllPaths<T[K], `${P}${K & string}.`> extends infer O
? `${O & string}` | `${P}${K & string}`
: never
这里的 infer O
是一种解决方法,因此 TypeScript 不会抱怨 Type instantiation is excessively deep and possibly infinite
。我们 return 递归调用的结果和当前路径都在这里,所以我们可以将 address
和 address.street
...作为稍后的键。
让我们在这里查看结果:
type T0 = AllPaths<IUser>
// type T0 = "id" | "name" | "balance" | "address" | "address.street" | "address.city" | "address.zipcode" | "address.tags"
我们现在需要一个类型,它接受一个路径并为我们获取正确的路径类型。
type PathToType<T, P extends string> = P extends keyof T
? T[P]
: P extends `${infer L}.${infer R}`
? L extends keyof T
? PathToType<T[L], R>
: never
: never
type T0 = PathToType<IUser, "address.street">
// type T0 = string
这也是递归类型。 P
这次从整条路径开始。
我们首先检查 P
是否是 keyof T
。如果是,我们可以return T[P]
的类型。如果不是,我们尝试将 P
拆分为两个文字类型 L
和 R
,它们必须用点分隔。如果左边的字符串文字匹配 T
的键,我们可以用 T[L]
和路径的其余部分递归调用 PathToType
。
最终我们在Matcher
类型中使用了两种类型。
type Matcher<T> = Partial<{
[K in AllPaths<T>]: PathToType<T, K & string>
}>
我们允许AllPaths<T>
中的任何字符串作为键,并使用PathToType<T, K & string>
获取相应类型的路径。
结果如下所示:
type T0 = Matcher<IUser>
// type T0 = {
// id?: number | undefined;
// name?: string | undefined;
// balance?: number | undefined;
// address?: Address | undefined;
// "address.street"?: string | undefined;
// "address.city"?: string | undefined;
// "address.zipcode"?: number | undefined;
// "address.tags"?: string[] | undefined;
//}