【问题标题】:How to implement a "GetTypeByPath" type in TypeScript?如何在 TypeScript 中实现“GetTypeByPath”类型?
【发布时间】:2022-02-18 12:15:57
【问题描述】:

我的要求是类型别名可以传入两个参数,第一个是对象类型,第二个是点表达式,表示字段的路径。

例如:

type Obj = {
  a: {
    b: string[]
  }
  c: number[]
}

GetTypeByPath<Obj, 'a.b'>  // => string[]
GetTypeByPath<Obj, 'c.5'>  // => number

那么GetTypeByPath 类型会是什么样子?

【问题讨论】:

  • this approach 是否适用于您的用例?如果没有,请告诉我我缺少什么。
  • @jcalz 哇..!!!这看起来很完美!谢啦!你能给我留点意见吗?因为这对我来说太神奇了..!
  • 有机会我会写一个答案的;不过,请先检查您的用例……这些“深度索引”操作往往有奇怪的边缘情况,不同的人希望看到不同的事情发生。我讨厌花太多时间在一个答案上,然后因为错过了一个用例而不得不改变整个事情。
  • @jcalz 我已经在我的用例中测试了您的解决方案,并且效果很好。对此,我真的非常感激!我想知道“Idx”实际上是如何工作的,“Idx”代表什么?
  • 我重构了我的 Idx 类型(代表“索引”,顺便说一句),使其更易于解释,其用途相同(但请再次检查!)

标签: typescript


【解决方案1】:

我之前回答过this sort of "deep indexing" question,但似乎总有一些稍微不同的用例。基本方法是使用template literal types 在点字符"." 处拆分路径字符串,并使用recursive conditional type 向下遍历嵌套属性。我会将您的 GetTypeByPath&lt;T, K&gt; 称为 DeepIndex&lt;T, K&gt; 以与此问题的先前实例保持一致。

无论如何,实现看起来像:

type DeepIndex<T, K extends string> = T extends object ? (
  K extends `${infer F}.${infer R}` ? DeepIndex<Idx<T, F>, R> : Idx<T, K>
) : never;

其中Idx&lt;T, K&gt; 表示“浅”indexed access 操作通常写为T[K]。稍后我们将讨论如何实现Idx&lt;T, K&gt;(以及为什么我们不直接写T[K])。

现在,让我们说服自己DeepIndex&lt;T, K&gt; 符合我们的预期。首先,我们不会尝试对原语进行索引,所以整个事情都包含在T extends object ? ( ... ) : never 中。我们可以对原语进行索引,但随后我们将支持DeepIndex&lt;Obj, 'a.b.5.length.toPrecision'&gt; 评估为(precision?: number | undefined) =&gt; string 之类的东西。如果您删除该包装器,那么您将得到这样的结果。

该类型的核心只是:尝试将第一个点处的路径拆分为字符串F 的第一部分和字符串R 的其余部分。如果你能做到这一点,那么你想将DeepIndex 放入路径RT[F] 属性中。如果不能,则不再有点,我们将路径 K 视为单个键并返回 T[K]


那么我们如何实现Idx&lt;T, K&gt;?一个相当普遍的复杂情况是,没有简单的方法可以向编译器表达K 处的路径片段肯定是T 的键之一。所以我们需要做的一件事就是检查。我们可以试试:

type Idx<T, K extends string> = K extends keyof T ? T[K] : never;

这样的作品:

type X = DeepIndex<Obj, 'a.b'>  // string[] ?

直到它没有:

type Y = DeepIndex<Obj, 'c.5'>  // never ?

这是您的用例与我见过的其他用例不同的地方。您正在使用 string"5"number[] 进行索引。但是the Array&lt;T&gt; type definition 有一个number index signature,而"5" 不是number。它是一个“数字字符串”,所以你应该可以用它来索引一个数组(因为 JavaScript 中的对象键是 actually strings and not really numbers),但编译器并不认为 "5" extends keyof (number[]) 是真的。

所以我们需要考虑这样的数字字符串键。让我们试试这个:

type Idx<T, K extends string> =
  K extends keyof T ? T[K] :
  K extends `${number}` ? number extends keyof T ? T[number] : never : never;

这里我们说如果K 是数字的字符串版本(microsoft/TypeScript#40598 中引入了“模式模板文字”`${number}` 语法),并且如果类型 T 具有数字索引签名,那么我们应该用number而不是K来索引T。让我们再次测试一下:

type X = DeepIndex<Obj, 'a.b'>  // string[] ?
type Y = DeepIndex<Obj, 'c.5'>  // number ?

看起来不错!


当然有大量的边缘情况。对于那些后来出现的问题,即使这满足了最初提出问题的人的需求,也不能保证它会按照您的预期进行......所以您应该针对所有用例进行真正的测试。

例如:在进行索引之前很难检查K 是否是有效路径。如果你写DeepIndex&lt;Obj, "something.that.is.definitely.not.a.path"&gt;,而不是编译器警告,你只会得到the never type,这可能会也可能不会被接受:

type Z = DeepIndex<Obj, 'oopsieDaisy'> // never

所以请仔细测试!

Playground link to code

【讨论】:

  • 精彩的解释!!再次,谢谢你!!而且,你能把这段代码包装成“源代码”吗? ---> type Idx&lt;T, K extends string&gt; = K extends keyof T ? T[K] : K extends ${number} ? number extends keyof T ? T[number] : never : never;
  • 糟糕,已修复。 ?
猜你喜欢
  • 2020-06-11
  • 1970-01-01
  • 2021-02-21
  • 1970-01-01
  • 2019-06-27
  • 2022-01-15
  • 1970-01-01
  • 2021-12-22
  • 1970-01-01
相关资源
最近更新 更多