【问题标题】:What does "homomorphic mapped type" mean?“同态映射类型”是什么意思?
【发布时间】:2021-11-11 21:21:29
【问题描述】:

我在一些 TypeScript PR 中看到了术语“同​​态映射类型”。这是一个例子:https://github.com/microsoft/TypeScript/pull/21919

在 --strictNullChecks 模式下,当同态映射类型删除 ?基础类型中的属性的修饰符,它还会从该属性的类型中删除 undefined

什么是同态映射类型?同态究竟是什么?有没有一个-同态映射类型的好例子

我感到困惑的原因是homomorphism 是保留特定操作的两个结构之间的映射。这里有问题的操作是什么?也就是说,其中f是映射,下面等式中op是什么:

f(x op y) = f(x) op f(y)

我尝试了什么

我尝试假设op&,这是两种类型相交的操作。

一个同态映射将是这样的:

F<T & U> = F<T> & F<U>

同态映射的一个例子(来自the TS handbook)是:

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

因为Partial&lt;T &amp; U&gt; 始终与Partial&lt;T&gt; &amp; Partial&lt;U&gt; 相同。

问题是我想不出任何方法来使映射类型非同态!

即使是像这样愚蠢的看起来也是同态的:

type Silly<T> = { [P in "foo"]: number}

让我感到困惑的是Silly 似乎是同态(Silly &lt;T &amp; U&gt; = Silly&lt;T&gt; &amp; Silly&lt;U&gt;)。

似乎handbook所说的同态映射类型相矛盾:

...同态,这意味着该映射仅适用于 T 的属性,而不适用于其他属性。编译器知道它可以在添加任何新的属性修饰符之前复制所有现有的属性修饰符

Silly 保留了&amp;,但不是手册中定义的同态映射类型。

【问题讨论】:

  • { [P in keyof T]: something possibly involving T[P] but not T } 形式的映射通过&amp;| 对类型的操作都是同态的。您的 Silly 类型是微不足道的同态;就像一个同态,它将一个组的所有元素映射到另一组的单位元素。我认为您从文档中引用的句子不是“同态”的良好定义。
  • @kaya3 你能想出一个映射类型的例子,它不是&amp;| 中的同态(根据“同态”的真正定义,而不是TS 中的那个)文档)?
  • 例如,Example&lt;T&gt; = { [k in keyof T]: T } 不是同态的,因为如果type Foo = {a: number}type Bar = {b: string} 那么Example&lt;Foo &amp; Bar&gt;{a: Foo &amp; Bar, b: Foo &amp; Bar}Example&lt;Foo&gt; &amp; Example&lt;Bar&gt;{a: Foo} &amp; {b: Bar}。同样适用于|
  • 不,同态映射类型与F&lt;T|U&gt;F&lt;T&amp;U&gt; 以及它们与F&lt;T&gt;F&lt;U&gt; 的关系无关。 type Example&lt;T&gt; = {[K in keyof T]: T} 确实是一个同态映射类型,因为如果你这样做Example&lt;{readonly a: string}&gt;,则结果类型的a 属性将为readonly。我会写一个答案。
  • 好的,所以如果我们认为固定KReadonlyProperty&lt;T, K extends keyof T&gt; 是对T 类型的一元运算,这使得属性K 只读,那么Mapped 是同态的,因为ReadonlyProperty&lt;Mapped&lt;T&gt;, K&gt;Mapped&lt;ReadonlyProperty&lt;T, K&gt;&gt; 相同。我想这确实符合数学定义,尽管说映射Mapped&lt;...&gt; 与映射ReadonlyProperty&lt;..., K&gt; 通勤会更自然。

标签: typescript


【解决方案1】:

在 TypeScript 中,同态映射类型特别是编译器识别出您正在映射现有对象类型的属性的类型。在这种情况下,输出对象类型的属性将具有与输入类型相同的 readonly 和/或可选 (?) 属性修饰符。我知道有几种方法可以使映射类型同态,还有一些其他方法可以使它......不是。

接下来,让我们使用这种类型作为映射对象:

type Foo = {
    norm: string,
    opt?: string,
    readonly ro: string,
    readonly both?: string
};

主要的同态映射类型技术,in keyof:

type Hom1<T> = { [P in keyof T]: number };
type Hom2<T, U> = { [K in keyof (T & U)]: K extends keyof T ? "L" : "R" };
type Hom3 = { [Q in keyof { readonly a: string, b?: number }]: Q };

在上面,您明确地迭代了 keyof 一些东西。让我们看看当我们在 Foo 类型上使用它们时你会得到什么:

type Hom1Foo = Hom1<Foo>;
/* type Hom1Foo = {
    norm: number;
    opt?: number | undefined;
    readonly ro: number;
    readonly both?: number | undefined;
}*/

type Hom2FooDate = Hom2<Foo, { z: boolean }>
/*type Hom2FooDate = {
    norm: "L";
    opt?: "L" | undefined;
    readonly ro: "L";
    readonly both?: "L" | undefined;
    z: "R";
} */

type Hom3Itself = Hom3
/* type Hom3Itself = {
    readonly a: "a";
    b?: "b" | undefined;
} */

您可以看到,在所有输出中,只读和可选标记都从输入中复制过来。这是生成同态映射类型的主要技术,也是迄今为止最常见的技术。


辅助同态映射类型技术,in K,其中Kextendskeyof T 是泛型类型参数,T 是泛型类型参数:

// <K extends keyof T, T> ... {[P in K]: ...}
type Hom4<T, K extends keyof T> = { [P in K]: 1 };

这特别使我们能够从对象的一些键复制属性修饰符,并且是implemented here,主要是为了使the Pick&lt;T, K&gt; utility type同态。让我们看看Hom4Foo 的表现如何:

type Hom4AllKeys = Hom4<Foo, keyof Foo>;
/* type Hom4AllKeys = {
    norm: 1;
    opt?: 1 | undefined;
    readonly ro: 1;
    readonly both?: 1 | undefined;
}*/

type Hom4SomeKeys = Hom4<Foo, "opt" | "ro">;
/* type Hom4SomeKeys = {
    opt?: 1 | undefined;
    readonly ro: 1;
}*/

现在几乎所有映射类型的其他用途都给出了一个-同态类型。如果您不认为自己映射到不同对象类型的键,这并不是真正的问题。例如:

type NonHom0 = { [P in "a" | "b" | "c"]: 0 };
/* type NonHom0 = {
    a: 0;
    b: 0;
    c: 0;
}*/

NonHom0 的属性既不是可选的也不是只读的;为什么会这样?没有其他类型带有键 abc 可以从中复制它们。如果您开始想象您正在从其他对象类型复制属性,事情会变得有点棘手,但编译器不会这样认为:

type NonHom1 = { [P in "norm" | "opt" | "ro" | "both"]: Foo[P] };
/* type NonHom = {
    norm: string;
    opt: string | undefined;
    ro: string;
    both: string | undefined;
}*/

type KeysOfFoo = keyof Foo
type NonHom2 = { [K in KeysOfFoo]: 1 }
/* type NonHom2 = {
    norm: 1;
    opt: 1;
    ro: 1;
    both: 1;
} */

type NonHom3 = { [Q in Extract<keyof Foo, string>]: Foo[Q] };
/* type NonHom3 = {
    norm: string;
    opt: string | undefined;
    ro: string;
    both: string | undefined;
}*/

在这些情况下,映射是非同态的;输出类型既没有只读属性也没有可选属性。 (| undefined 仍然存在于曾经是可选的属性上,但属性本身不是可选的)。确实,您仍在迭代 Foo 的键,但编译器不再看到与 Foo 的关系。在NonHom1 中,键恰好相同,但没有keyof,因此编译器不会将映射识别为同态。在NonHom2 中,您使用的是keyof,但编译器急切地评估KeysOfFoo,因此当您到达NonHom2 时,它与NonHom1 中的映射相同。在NonHom3 中,您仅迭代Foostring 键(即全部),但编译器再次丢失线程并且不再将in Extract&lt;keyof Foo, string&gt; 识别为同态映射的触发器。有workarounds, see microsoft/TypeScript#24679,但这里的重点是,如果您偏离in keyofin K-where-K extends keyof T-and-both-K-and-T-are-generic,您将得到非同态映射。


哇,我说完了。我不想在这之后的几天里写“同态”这个词。无论如何,希望有所帮助;祝你好运!

Playground link to code

【讨论】:

  • 谢谢@jcalz。同态映射类型如何同态? f(x op y) = f(x) op f(y) 中的 fop 是什么?
  • 该术语似乎已在this pull request 中引入,它也被称为“结构保留”。我不确切知道预期的同态是什么。我只能猜测他们在谈论与哪些属性是只读和/或可选的有关的 arity-1(一元)运算符,而不是像您的 op 这样的任何二元运算符。例如,假设有一个一元运算符Opts&lt;T&gt;,它返回T 中的可选键列表,然后Opts&lt;F&lt;T&gt;&gt; = F&lt;Opts&lt;T&gt;&gt; 用于同态映射类型F&lt;T&gt;
  • 来自github.com/microsoft/TypeScript/issues/31025 - 谢谢你的解释!!
  • 这对我来说意义重大,因为我正在迭代交叉路口的键。将keyof A &amp; keyof B 更改为keyof (A &amp; B) 会产生很大的不同。谢谢TS精灵jcalz!
猜你喜欢
  • 2015-01-03
  • 2018-06-29
  • 2010-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-20
  • 1970-01-01
  • 2020-04-17
相关资源
最近更新 更多