具有模板文字类型的属性路径

rsm*_*idt 3 typescript

通过添加模板文字类型,现在可以以类型安全的方式表达属性路径(点符号)。一些用户已经使用模板文字类型实现了一些东西或者提到了它

我想更进一步,并表达类型中空值/未定义/可选的可能性,例如foo.bar?.foobarfoo.boo.far?.farboo编译器应该可以接受,foo.bar.foobar而不适用于以下类型:

type Test = {
  foo: {
    bar?: {
      foobar: never;
      barfoo: string;
    };
    foo: symbol;
    boo: {
      far:
        | {
            farboo: number;
          }
        | undefined;
    };
  };
};
Run Code Online (Sandbox Code Playgroud)

到目前为止,可选参数被拾取(我不知道为什么,但它在我的 IDE 中使用相同的打字稿版本,请参见下面的屏幕截图)但不是明确标记的“远”属性作为未定义。这个游乐场展示了我的进步。不知何故,“未定义检查”没有按预期工作。

IDE 正确展开

jca*_*alz 8

请注意:即使语言支持递归条件类型,深度索引操作也很容易与编译器的递归限制器发生冲突。即使相对较小的更改也可能意味着似乎可以工作的版本与使编译器陷入困境或发出可怕错误的版本之间的差异:“ ? Type instantiation is excessively deep and possibly infinite. ?”。DeepKeyOf这里介绍的版本似乎有效,但它绝对是在循环的深渊之上走钢丝。

附加警告:这样的事情总是有各种边缘情况。你可能不乐意与如何(或有)的版本DeepKeyOf<XYZ>在情况下,手柄类型的东西XYZ:有一个指数的签名; 是类型的联合;是递归的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}`;
Run Code Online (Sandbox Code Playgroud)

为了确定,让我们测试一下Test

type DeepKeyOfTest = DeepKeyOf<Test>
// type DeepKeyOfTest = "foo.foo" | "foo.bar?" | "foo.bar?.foobar" | "foo.bar?.barfoo" 
//  | "foo.boo.far?" | "foo.boo.far?.farboo"
Run Code Online (Sandbox Code Playgroud)

看起来挺好的。


让我们来看看它是如何工作的:

type DeepKeyOf<T> = (
  [T] extends [never] ? "" :
Run Code Online (Sandbox Code Playgroud)

在这里,我们将DeepKeyOf<never>显式返回空字符串""。如果您想主要分布DeepKeyOf<T>在联合中,T同时仍然具有 never显示类型的属性,则需要类似的东西。正如我在评论中所说的那样,我对这是一种理想的行为持怀疑态度。分布在联合上很好,因为它自动DeepKeyOf<{a: string} | undefined>等同于DeepKeyOf<{a: string}> | DeepKeyOf<undefined>. 但是DeepKeyOf<never>真的应该是never,要保持一致(因为任何类型X都等价于X | never)。无论如何,这又归结为边缘情况,所以我将继续:

  T extends object ? (
Run Code Online (Sandbox Code Playgroud)

如果T不是原始类型,那么我们将生成某种类型的键。请注意,如果重要的话,数组和函数不是基元。

    { [K in Exclude<keyof T, symbol>]:
Run Code Online (Sandbox Code Playgroud)

除了任何可能的值键之外,我们将首先使用相同的键创建一个映射类型。删除对于允许在模板文字类型中使用每个键很重要。TsymbolsymbolK

      `${K}${undefined extends T[K] ? "?" : ""}${DotPrefix<DeepKeyOf<T[K]>>}` }
Run Code Online (Sandbox Code Playgroud)

这是该类型的主力。对于每个键,K我们以K. 然后,如果 key 处的属性类型K,即T[K]可以接受一个undefined值,我们追加"?". 最后,我们追加DotPrefix<DeepKeyOf<T[K]>>,其中DeepKeyOf<T[K]>预期是属性的所有键的并集T[K],并DotPrefix负责可选地包含"."字符,如下所述。

          [Exclude<keyof T, symbol>]
Run Code Online (Sandbox Code Playgroud)

我们现在创建的映射类型看起来像{a: "a.foo" | "a.bar"; b: "b"},但我们想要类似的东西"a.foo" | "a.bar" | "b"。我们通过使用我们用来创建它的相同键索引到映射类型来做到这一点。

  ) : ""
Run Code Online (Sandbox Code Playgroud)

如果T既不是never也不是原语,我们将产生空字符串""。所以DeepKeyOf<string>""

) extends infer D ? Extract<D, string> : never;
Run Code Online (Sandbox Code Playgroud)

这行真的不应该是必需的,但它可以防止递归深度警告。本质上,通过写入,extends infer D我们将结果复制到一个新参数中,D并导致编译器推迟评估,否则它会急切地执行。在Extract<D, string>让编译器明白,DeepKeyOf<T>总是会产生的一个亚型string,这样的递归步骤会成功。

最后,

type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
Run Code Online (Sandbox Code Playgroud)

将采取类似"foo" | "bar" | ""和产生的东西".foo" | ".bar" | ""。除非该输入是空字符串,否则它会在其输入前添加一个点。没有这样的例外,你会有像"foo.bar.baz."这样的类型以点结尾。

Playground 链接到代码