【问题标题】:Property path with template literal types具有模板文字类型的属性路径
【发布时间】:2021-04-04 01:10:48
【问题描述】:

通过添加template literal types,现在可以以类型安全的方式表达属性路径(dot notation)。一些用户已经使用模板文字类型或mentioned it 拥有implemented 的东西。

我想更进一步,并表达类型中 nulls/undefined/optionals 的可能性,例如foo.bar?.foobarfoo.boo.far?.farboo 应该可以被编译器接受,而 foo.bar.foobar 不适合以下类型:

type Test = {
  foo: {
    bar?: {
      foobar: never;
      barfoo: string;
    };
    foo: symbol;
    boo: {
      far:
        | {
            farboo: number;
          }
        | undefined;
    };
  };
};

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

【问题讨论】:

  • this 是否适用于您的用例?如果是这样,我会写一个答案;如果没有,请详细说明。顺便说一句,我对never 类型的任何属性都持怀疑态度。我不确定foo.bar?.foobar 的用例应该是什么,但在我担心尝试使DeepKeyOf 保留never 类型的属性的键之前,我想听听一个引人注目的用例.
  • @jcalz 这绝对有效)
  • @jcalz,非常感谢!这似乎确实工作得很好。我只是想介绍never 案例。我没有其他理由。
  • 好的,有机会我会回过头来写一个答案

标签: typescript


【解决方案1】:

请注意:即使语言支持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&lt;never&gt; 显式返回空字符串""。如果您想在 T 中的联合上主要分配 DeepKeyOf&lt;T&gt;,同时仍然具有类型为 only never 的属性,则需要类似的东西。正如我在 cmets 中所说,我对这是理想的行为有点怀疑。通过联合分布很好,因为它自动使DeepKeyOf&lt;{a: string} | undefined&gt; 等同于DeepKeyOf&lt;{a: string}&gt; | DeepKeyOf&lt;undefined&gt;。但是DeepKeyOf&lt;never&gt; 真的应该是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&lt;DeepKeyOf&lt;T[K]&gt;&gt;,其中DeepKeyOf&lt;T[K]&gt; 应该是属性T[K] 的所有键的联合,DotPrefix 负责选择性地包括"." 字符,如下所述。

          [Exclude<keyof T, symbol>]

我们创建的映射类型现在看起来像{a: "a.foo" | "a.bar"; b: "b"},但我们想要像"a.foo" | "a.bar" | "b" 这样的东西。为此,我们使用创建它时使用的相同键对映射类型进行索引。

  ) : ""

如果T 既不是never 也不是原语,我们将生成空字符串""。所以DeepKeyOf&lt;string&gt; 将是""

) extends infer D ? Extract<D, string> : never;

这一行确实没有必要,但它可以防止递归深度警告。本质上,通过编写extends infer D,我们将结果复制到一个新参数D 并导致编译器推迟评估,否则它会急切地执行。 Extract&lt;D, string&gt; 让编译器了解DeepKeyOf&lt;T&gt; 将始终生成string 的子类型,因此递归步骤将成功。

最后,

type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;

将采用"foo" | "bar" | "" 之类的内容并生成".foo" | ".bar" | ""。除非该输入是空字符串,否则它会在其输入前添加一个点。如果没有这样的例外,您将拥有像 "foo.bar.baz." 这样以点结尾的类型。

Playground link to code

【讨论】:

  • 它工作得很好,但是如果我使用字符串连接或可变参数而不是硬编码路径,TS 会返回错误。如果我添加一个字符串选项 type DeepKeyOfTest = DeepKeyOf&lt;Test&gt; | string ,错误就消失了,但自动完成也是如此。你知道怎么解决吗?
  • 通常您希望编译器拒绝 string,因为它不能确定它是否符合DeepKeyOf&lt;Test&gt;。如果您想接受任何string 但保持自动完成提示,那么您正在寻找this
  • 感谢它的工作原理!
  • 如何从路径中获取价值?我有一个案例,从 { foo: { bar: number } | 从路径 'foo.bar' 中提取值number },期望结果是 number 而不是 never
  • @Zheeeng cmets 在一个老问题上并不是获得问题答案的最佳场所。不过,如果您查看this question,您可以使用该答案中的代码进行深度索引。喜欢this。如果这对您有所帮助,请考虑支持the answer
猜你喜欢
  • 1970-01-01
  • 2014-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多