繁体   English   中英

扁平化界面

[英]Flatten an interface

我有一个用例,我希望能够基于泛型类型派生我的类型的键/字段。

例如:

这些是容器接口

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'
}

如何更新我的模型(匹配器)以支持此用例?

免责声明:这些类型的问题总是有很多边缘情况。 此解决方案适用于您的示例,但在我们引入可选属性或联合等内容时可能会中断。 此解决方案也不包括数组内的对象路径。

该解决方案包括两个主要部分:

首先,我们需要一个类型,它将采用类型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附加到当前路径并返回它。

T[K] extends object 
  ? T[K] extends any[] 
    ? `${P}${K & string}` 
    : /* ... */
  : `${P}${K & string}`

如果T[K]是一个对象,我们可以递归调用AllPaths ,以T[K]作为新的T并再次以当前键为当前路径。 这次我们还附加了一个"." 到路径,因为我们知道稍后将附加一个嵌套属性。

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的。 我们在这里返回递归调用的结果和当前路径,因此我们可以稍后将addressaddress.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 如果是,我们可以返回T[P]的类型。 如果不是,我们尝试将P拆分为两个字面量类型LR ,它们必须用点分隔。 如果左侧字符串文字与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;
//}

操场

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM