如何实现一个 TypeScript 泛型类型,用于嵌套属性,以解析它们的类型?

huangapple go评论67阅读模式
英文:

How to implement a TypeScript generic type for nested properties that resolves to their types?

问题

以下是翻译好的部分:

如何创建一个通用类型,该类型解析为嵌套属性的类型

假设我有一个配置类型

```typescript
type Config = {
  parent: {
    child: boolean
  }
}

我想要类似于以下的东西:

type ChildConfigType = NestedType<Config, 'parent.child'> // boolean
type ParentConfigType = NestedType<Config, 'parent'> // {child: boolean}

我有一个用于非嵌套类型的基本实现

function getProp<Type extends object, Key extends keyof Type>(key: Key): Type[Key] {
    return {} as Type[Key]
}

getProp<Config>('parent')

我还有一些非嵌套的辅助类型,可以正常工作

type PropsOf<Type extends object> = keyof Type

type TypeOfProp<Type extends object, Key extends PropsOf<Type>> = Type[Key]

function getPropWithHelpers<Type extends object, Key extends PropsOf<Type>>(key: Key): TypeOfProp<Type, Key> {
    return {} as TypeOfProp<Type, Key>
}

我还有一个解析为嵌套属性的扁平联合的NestedPropsOf辅助类型

type NestedPropsOf<Type extends object> =
  {
    [key in keyof Type & (string | number)]: Type[key] extends object
    ? `${key}` | `${key}.${NestedPropsOf<Type[key]}>`
    : `${key}`
  }[keyof Type & (string | number)]

type NestedConfigProps = NestedPropsOf<Config> // 'parent' | 'parent.child'

我试图实现一个带有以下签名的函数

function getNestedProp<Type extends object, Key extends NestedPropsOf<Type>>(key: Key): NestedTypeOfProp<Type, Key> {
    return {} as NestedTypeOfProp<Type, Key>
}

getNestedProp<Config>('parent.child')

我开始相信这可能是不可能的,因为类型无法直接操作字符串,所以我无法删除路径的第一个段。

Ts Playground


<details>
<summary>英文:</summary>

How to create a generic type that resolves to the type of a nested property

Say I have a configuration type

type Config = {
parent: {
child: boolean
}
}

I would like something similar to

type ChildConfigType = NestedType<Config, 'parent.child'> // boolean
type ParentConfigType = NestedType<Config, 'parent'> // {child: boolean}




I have a basic implementation for non nested types

function getProp<
Type extends object,
Key extends keyof Type
>(key: Key): Type[Key] {
return {} as Type[Key]
}

getProp<Config>('parent')


I also have non nested helper types that work

type PropsOf<
Type extends object
> = keyof Type

type TypeOfProp<
Type extends object,
Key extends PropsOf<Type>
> = Type[Key]

function getPropWithHelpers<
Type extends object,
Key extends PropsOf<Type>
>(key: Key): TypeOfProp<Type, Key> {
return {} as TypeOfProp<Type, Key>
}


I also have a NestedPropsOf helper that resolves to a flattened union of the nested properties

type NestedPropsOf<Type extends object> =
{
[key in keyof Type & (string | number)]: Type[key] extends object
? ${key} | ${key}.${NestedPropsOf&lt;Type[key]&gt;}
: ${key}
}[keyof Type & (string | number)]

type NestedConfigProps = NestedPropsOf<Config> // 'parent' | 'parent.child'


And I am trying to implement a function with the signature

function getNestedProp<
Type extends object,
Key extends NestedPropsOf<Type>
>(key: Key): NestedTypeOfProp<Type, Key> {
return {} as NestedTypeOfProp<Type, Key>
}

getNestedProp<Config>('parent.child')


I am starting to believe this is not possible because the types can&#39;t directly manipulate strings so I cannot remove the first segment of the path.

[Ts Playground][1]


  [1]: https://www.typescriptlang.org/play?#code/C4TwDgpgBAwg9gOwGYEsDmUC8UDeAoKQqMAQwCcIFgAuXAohgYwAsUAbAE1oCM442IJBPUIBfPOLxIArgkbAUiKGgjAACmThgAPCKgAVcNAgAPYJQ4BnKHG4ArCPIA0egNIQQUU%20YRWoAaw84JAMjPAA%20AApAkFp3EABKWkNIAG14gF06BgpgaTIEXFEoEmsUiHSPDIk8WoB6OqgAVUsUBAxmCDZIMihQSEta-ugNLUsAeSRdBnKvMwtrWwd5CKwAoJDyoaNQyEnRnT1Z7wWbe0dgFwZ4uZ8-A4mp8vDV7HLKkGqpWXlFQpV1JowAB1FDAZgACS6PUs0yIx3mvkW52cbg8t1OD0m2meEWiHjiHiSuwg%20yBOKMTig8XC2SIuXyhRwxVKJLJWgpkCpNJq9UaLTaGAQEEs5g4UE63QgZEGeGGUAAciKxVinjsTkizstgLTMPR8AxUjEoG11iBgiSoAAyKCRUVkQVQAA%20UAQ0gAttxpQkMskjEaqhjNUsLnoAPxQAAGABIcDFRJHnVHY-GAHSxpWiiAcVWcioxDLhBN6WgxuMeYtiAPmzY7G124AO9pJt2e71feUAWRADzWmZVQMe2hwpAoVFoOBY7C4UF4-EECFEohecp2-ez5XZhxm6sRfhDqOu6I1fnXOcH2OeSftgtedMIDSgAHdmCRgLz5WpyJRgLNsGfNyQA5tHgZB0CpAByUcfwgl55RgVhOD-RVlQ3IwtxAxBUDQSDoKoVMp04WDahkOQFCUAEz2Ao5dzuZFtSuIgbhPawqIvNVIBeKIYkJRJaAA9CgPJcpuQ8WkDXpVRGSKEpWNQjhAOAkTqTEiQgA

</details>


# 答案1
**得分**: 2

以下是翻译好的部分:

可以使用 [template literals](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#inference-with-template-literals) 和 [infer](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types) 关键字来操纵字符串。

实用工具:

`Dot` 用于连接两个字符串,使用 `.`:
```typescript
type Dot<T extends string, U extends string> = "" extends U ? T : `${T}.${U}`;

StopFields 是应该在类型中停止继续的类型;在我们的情况下,这些是基本类型:

type StopFields = string | number | boolean | symbol;

PathToFields 使用 mapped types 返回传递类型属性的所有可能路径。它会忽略数组,可以进行调整。

type PathsToFields<T> = T extends StopFields
  ? ""
  : {
      [K in Extract<keyof T, string>]: NonNullable<T[K]> extends Array<any>
        ? never
        : K | Dot<K, PathsToFields<T[K]>>;
    }[Extract<keyof T, string>];

实现:

type NestedType<T, Path extends PathsToFields<T>> = Path extends keyof T
  ? T[Path]
  : Path extends `${infer Property extends keyof T & string}.${infer SubField}`
  ? SubField extends PathsToFields<T[Property]>
    ? NestedType<T[Property], SubField>
    : never
  : never;

NestedType 期望一个类型和其字段路径。它首先检查路径是否是对象的属性,如果是,返回该属性下的类型,否则使用 . 分割路径,左侧部分应该是传递对象的键,. 后面是剩余路径。如果所有条件都为真,我们会调用 NestedType 以获取从路径分割中获得的属性和传递的剩余路径。

用法:

type ChildConfigType = NestedType<Config, "parent.child">; // boolean
type ParentConfigType = NestedType<Config, "parent">; // {child: boolean}

playground

英文:

It is actually possible to manipulate strings using template literals and infer keyword.

Utilities:

Dot is used to concatenate two strings with .:

type Dot&lt;T extends string, U extends string&gt; = &quot;&quot; extends U ? T : `${T}.${U}`;

StopFields is the type on which we should stop going further in the types; in our case, those are primitives:

type StopFields = string | number | boolean | symbol;

PathToFields returns all possible paths to properties of the passed type using mapped types. It ignores arrays, which can be adjusted.

type PathsToFields&lt;T&gt; = T extends StopFields
  ? &quot;&quot;
  : {
      [K in Extract&lt;keyof T, string&gt;]: NonNullable&lt;T[K]&gt; extends Array&lt;any&gt;
        ? never
        : K | Dot&lt;K, PathsToFields&lt;T[K]&gt;&gt;;
    }[Extract&lt;keyof T, string&gt;];

Implementation:

type NestedType&lt;T, Path extends PathsToFields&lt;T&gt;&gt; = Path extends keyof T
  ? T[Path]
  : Path extends `${infer Property extends keyof T &amp; string}.${infer SubField}`
  ? SubField extends PathsToFields&lt;T[Property]&gt;
    ? NestedType&lt;T[Property], SubField&gt;
    : never
  : never;

NestedType expects a type and its paths to fields. It first checks if the path is the property of the object, in that case, returns the type under that property, otherwise splits the path with . where the left part should be the key of the passed object and the part after the . is the remaining path. If all conditions are true, we invoice NestedType for the property that we got from splitting the path and passing the remaining path.

Usage:

type ChildConfigType = NestedType&lt;Config, &quot;parent.child&quot;&gt;; // boolean
type ParentConfigType = NestedType&lt;Config, &quot;parent&quot;&gt;; // {child: boolean}

playground

huangapple
  • 本文由 发表于 2023年6月2日 01:30:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/76384345.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定