【问题标题】:How does this DeepPartial type work from Typescript?这种 DeepPartial 类型如何在 Typescript 中工作?
【发布时间】:2021-04-18 14:51:51
【问题描述】:

我在这里查看了这个问题:

TypeScript: deep partial?

最佳答案的 DeepPartial 类型定义如下:

type DeepPartial<T> = {
    [P in keyof T]?: DeepPartial<T[P]>;
};

我测试过,这种类型按预期工作,但我不能完全理解它在内部是如何工作的。

让我们以同一个问题的界面为例:

interface Foobar {
  foo: number;
  bar: {
    baz: boolean;
    qux: string;
  };
}

当P=bar时,很容易理解它会递归进入

DeepPartial<{
    baz: boolean;
    qux: string;
  }>

并使“baz”和“qux”键可选。

但我不明白的是,原始类型的递归是如何工作的? 就像foo 键一样,DeepPartial&lt;number&gt;number 有何相同之处?当 T=number 时,

[P in keyof number]?: DeepPartial<number[P]>;

对我来说没有意义。

在我看来,DeepPartial 的实现应该是这样的:

type DeepPartial<T> = {
    [P in keyof T]?: isPrimitive(T[P]) ? T[P] : DeepPartial<T[P]>;
};

但原始实现也可以,但我不明白如何。

我希望我能很好地解释我的问题。这是示例游乐场:

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgGIHt0CM5WQbwChlkZMAuZEAVwFstoBuY5HKSokknAL0q0wAbCHBDMuyAI7UAHpQDOYKKADm45AF9mGwoTABPAA4oAIhAiGACrjDA4ggDwAVAHzIAvARYkA2peSgyADWEProMMhOALoA-JRmFtZQtvbOflEu2syECOggiqSYbPHmVjZ2jhjYuG6enIXolACMADQsxQSscHzIStQoOlq6uflgvRCKAJLgJYnlqTT00LXIAKzMQA

编辑: 我进一步调查发现

type DeepPartial<T> = {
    [P in keyof T]?: any;
};

type DeepPartialTest<T> = {
        [P in keyof number]?: any;
    };

现在 DeepPartial&lt;number&gt; 将返回 number 类型,但 DeepPartialTest&lt;number&gt; 不会。这让我觉得更奇怪。

编辑2添加截图:

这按预期工作:

这不是:

什么时候实际上两者应该是一样的??

【问题讨论】:

  • DeepPartial 只是使键可选。 number 是一个原始人,在技术上没有密钥,所以 DeepPartial&lt;number&gt; 是一个无操作
  • 但 DeepPartial 在这种情况下不应该返回空对象 ({}) 类型而不是“数字”类型吗?
  • 不,不应该。原语在技术上没有方法,当您在其上使用任何方法时,它只是转换为对象。你不能声称有一个数字(例如,42),但你不能以某种方式做到(42).toString(),因为你不能从原语中删除toString()。它们是不可变的。
  • 仍然不明白 DeepPartial 如何返回数字类型。因此,如果我将 DeepPartial 类型定义为没有键的对象(因为 keyof number 没有键),我得到的只是 T 而不是没有键的对象?无法理解逻辑是如何工作的
  • number 是原语,而不是对象。基元是不可变的,因此在 542 上具有不同的属性是没有意义的。

标签: typescript


【解决方案1】:

这实际上是 TypeScript 中用于类型参数的内部机制,它指出:

当在同构映射中用原始类型替换 T 类型,我们只需生成该原始类型。

您可以在pull request 中了解更多信息。

【讨论】:

    【解决方案2】:

    我认为问题是“为什么作用于基元的映射类型会产生相同的基元,为什么它似乎只是有时发生”而不是DeepPartial 本身,对吧?


    Mapped types 有两种“口味”。有一些普通的映射类型只遍历属性键字面量的任意联合(例如,{[P in K]: ...}),而 同态 映射类型专门遍历另一种类型的键(例如,{[P in keyof T]: ...} ),并尝试保留该类型的结构,例如,将readonly? 等修饰符从输入类型复制到输出类型(除非您使用自己的@ 更改修饰符987654329@ 或 ?+-)。示例:

    interface Obj {
      a?: number,
      b: string,
      readonly c: boolean
    }
    
    type Homomorphic<T> = {
      [K in keyof T]: Array<T[K]>
    }
    type ObjHom = Homomorphic<Obj>
    /* type ObjHom = {
        a?: (number | undefined)[] | undefined;
        b: string[];
        readonly c: boolean[];
    } */
    
    type ObjKeys = keyof Obj;
    
    type ObjNonHom = {
      [K in ObjKeys]: Array<Obj[K]>
    }
    /* type ObjNonHom = {
        a: (number | undefined)[];
        b: string[];
        c: boolean[];
    } */
    

    在上面的代码中,ObjHomObjNonHom是相似的,但是ObjHoma属性是可选的,它的c属性是readonly(就像在Obj中一样);而ObjNonHom 的属性都是必需且可变的。


    能够确定特定映射类型何时是同态的可能很棘手,因为涉及到很多启发式方法。一般来说,具有in keyof T 的映射类型,其中T 是某种类型,具体取决于未解析的泛型参数,这将是同态的。一般来说,以in X 为特征的映射类型,其中X 是某种类型,其中没有直接包含keyof,这将是非同态的。当映射类型特征 in keyof T 其中T 是一些非泛型类型时,事情就变得不确定了。对于像number 这样的原语,这往往是非同态的,但对于对象类型,它往往是同态的。我可能可以通过 GitHub 找到实现每个启发式的 PR,但我不知道这是否值得任何人花时间。


    microsoft/TypeScript#12447 中介绍了原语的特定行为。在描述中,它说:

    { [P in keyof T]: X } 形式的映射类型,其中T 是某个类型参数,被称为[同态] 映射类型,因为它生成的类型与T 具有相同的形状。 [...] [当] 原始类型替换同构映射类型中的T 时,我们只需生成该原始类型。例如,当{ [P in keyof T]: X }A | undefined 为T 实例化时,我们产生{ [P in keyof A]: X } | undefined


    所以这个问题的基本答案是:当映射类型是同态的并且作用于原始类型时,会出现相同的原始类型。原来的DeepPartial是同态的,所以number变成number。您直接使用in keyof number 的修改版本是非同态的,因此number 成为一个类型,number 的每个明显属性都映射到一个可选属性。

    Playground link to code

    【讨论】:

      猜你喜欢
      • 2020-09-03
      • 2019-02-20
      • 2019-12-16
      • 2021-02-24
      • 1970-01-01
      • 1970-01-01
      • 2022-01-26
      • 2019-07-05
      • 1970-01-01
      相关资源
      最近更新 更多