请注意:即使语言支持recursive conditional types,深度索引操作也很容易与编译器的递归限制器发生冲突。即使是相对较小的更改也可能意味着似乎可以工作的版本与使编译器陷入困境或发出可怕错误的版本之间的差异:“⚠ Type instantiation is excessively deep and possibly infinite. ⚠”。这里介绍的DeepKeyOf 版本似乎有效,但它绝对是在循环深渊之上走钢丝。
附加警告:这样的事情总是有各种各样的边缘情况。在XYZ: 类型为index signature; 的情况下,您可能对DeepKeyOf<XYZ> 的这个(或任何)版本如何处理事情不满意;是一个union 类型;像type Recursive = { prop: Recursive }; 一样递归;等等。有可能对于每个边缘情况都有一个调整,在您看来会表现得“更好”,但处理所有这些情况可能超出了这个问题的范围。
好的,警告结束。来看看DeepKeyOf<T>:
type DeepKeyOf<T> = (
[T] extends [never] ? "" :
T extends object ? (
{ [K in Exclude<keyof T, symbol>]:
`${K}${undefined extends T[K] ? "?" : ""}${DotPrefix<DeepKeyOf<T[K]>>}` }[
Exclude<keyof T, symbol>]
) : ""
) extends infer D ? Extract<D, string> : never;
type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
为了确定,让我们在 Test 上测试一下:
type DeepKeyOfTest = DeepKeyOf<Test>
// type DeepKeyOfTest = "foo.foo" | "foo.bar?" | "foo.bar?.foobar" | "foo.bar?.barfoo"
// | "foo.boo.far?" | "foo.boo.far?.farboo"
看起来不错。
让我们来看看它是如何工作的:
type DeepKeyOf<T> = (
[T] extends [never] ? "" :
这里我们将让DeepKeyOf<never> 显式返回空字符串""。如果您想在 T 中的联合上主要分配 DeepKeyOf<T>,同时仍然具有类型为 only never 的属性,则需要类似的东西。正如我在 cmets 中所说,我对这是理想的行为有点怀疑。通过联合分布很好,因为它自动使DeepKeyOf<{a: string} | undefined> 等同于DeepKeyOf<{a: string}> | DeepKeyOf<undefined>。但是DeepKeyOf<never> 真的应该是never,以保持一致(因为任何类型的X 都等效于X | never)。无论如何,这又回到了边缘情况,所以我会继续前进:
T extends object ? (
如果T 不是原始类型,那么我们将生成某种类型的键。请注意,如果重要的话,数组和函数不是基元。
{ [K in Exclude<keyof T, symbol>]:
我们将首先使用与T 相同的键创建mapped type,但任何可能的symbol 值键除外。删除 symbol 很重要,以允许在 template literal types 中使用每个键 K。
`${K}${undefined extends T[K] ? "?" : ""}${DotPrefix<DeepKeyOf<T[K]>>}` }
这是该类型的主力。对于每个键K,我们以K 开始一个新字符串。然后,如果键K 处的属性类型,即T[K] 可以接受undefined 值,我们附加"?"。最后,我们追加DotPrefix<DeepKeyOf<T[K]>>,其中DeepKeyOf<T[K]> 应该是属性T[K] 的所有键的联合,DotPrefix 负责选择性地包括"." 字符,如下所述。
[Exclude<keyof T, symbol>]
我们创建的映射类型现在看起来像{a: "a.foo" | "a.bar"; b: "b"},但我们想要像"a.foo" | "a.bar" | "b" 这样的东西。为此,我们使用创建它时使用的相同键对映射类型进行索引。
) : ""
如果T 既不是never 也不是原语,我们将生成空字符串""。所以DeepKeyOf<string> 将是""。
) extends infer D ? Extract<D, string> : never;
这一行确实没有必要,但它可以防止递归深度警告。本质上,通过编写extends infer D,我们将结果复制到一个新参数D 并导致编译器推迟评估,否则它会急切地执行。 Extract<D, string> 让编译器了解DeepKeyOf<T> 将始终生成string 的子类型,因此递归步骤将成功。
最后,
type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
将采用"foo" | "bar" | "" 之类的内容并生成".foo" | ".bar" | ""。除非该输入是空字符串,否则它会在其输入前添加一个点。如果没有这样的例外,您将拥有像 "foo.bar.baz." 这样以点结尾的类型。
Playground link to code