我之前回答过this sort of "deep indexing" question,但似乎总有一些稍微不同的用例。基本方法是使用template literal types 在点字符"." 处拆分路径字符串,并使用recursive conditional type 向下遍历嵌套属性。我会将您的 GetTypeByPath<T, K> 称为 DeepIndex<T, K> 以与此问题的先前实例保持一致。
无论如何,实现看起来像:
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<T, K> 表示“浅”indexed access 操作通常写为T[K]。稍后我们将讨论如何实现Idx<T, K>(以及为什么我们不直接写T[K])。
现在,让我们说服自己DeepIndex<T, K> 符合我们的预期。首先,我们不会尝试对原语进行索引,所以整个事情都包含在T extends object ? ( ... ) : never 中。我们可以对原语进行索引,但随后我们将支持DeepIndex<Obj, 'a.b.5.length.toPrecision'> 评估为(precision?: number | undefined) => string 之类的东西。如果您删除该包装器,那么您将得到这样的结果。
该类型的核心只是:尝试将第一个点处的路径拆分为字符串F 的第一部分和字符串R 的其余部分。如果你能做到这一点,那么你想将DeepIndex 放入路径R 的T[F] 属性中。如果不能,则不再有点,我们将路径 K 视为单个键并返回 T[K]。
那么我们如何实现Idx<T, K>?一个相当普遍的复杂情况是,没有简单的方法可以向编译器表达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<T> 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<Obj, "something.that.is.definitely.not.a.path">,而不是编译器警告,你只会得到the never type,这可能会也可能不会被接受:
type Z = DeepIndex<Obj, 'oopsieDaisy'> // never
所以请仔细测试!
Playground link to code