Skip to content

协变和逆变 #22

@astak16

Description

@astak16

协变和逆变的概念网上一搜一大把,就不说了,说概念一定会把人绕晕,举个例子来说就容易理解了。

假设现在有个父子关系: ParentSon

interface Parent {
  parentProp: string;
}
interface Son extends Parent {
  sonProp: string;
}

type IsCo = Son extends Parent ? true : false; // true

变化规则为:f ⇒ 把 ParentSon 变为数组:

type ParentArray = Parent[];
type SonArray = Son[];

type IsCo = SonArray extends ParentArray ? true : false; // true

因为 SonParent 的子类型,他们都变为数组后,还是满足 SonParent 的子类型,所以这个叫做协变。

此时把变化规则 f 改为函数 ⇒ 两个函数分别接收 ParentSon

type ParentFn = (param: Parent) => void;
type SonFn = (param: Son) => void;

type IsContra = ParentFn extends SonFn ? true : false; // true

此时他们的继承关系反过来了,因为 ParentSon 的子集,或者叫安全类型,所以这叫做逆变

就有下面的公式

  • f(Son) ≤ f(Parent) 变化规则 f 被称为:协变
  • f(Parent) ≤ f(Son) 变化规则 f 被称为:逆变

逆变的应用

逆变最常见的应用是函数重载

function fn(x: never): void;
function fn(x: string): void {}

// 等价于

function fn(x: number & string): void;
function fn(x: string): void {}

neverstring 的子类型。

never 也可以是交叉类型的结果,如 number & stringnever

题目

题目链接:UnionToIntersection

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<UnionToIntersection<"foo" | 42 | true>, "foo" & 42 & true>>,
  Expect<
    Equal<
      UnionToIntersection<(() => "foo") | ((i: 42) => true)>,
      (() => "foo") & ((i: 42) => true)
    >
  >
];

答案为

type UnionToIntersection<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;
  1. 首先将联合类型变为 (k: Union) => void 的形式:

    "foo" | 42 | true -> ((k: "foo") => void) | ((k: 42) => void) | ((k:  true) => void)
  2. 根据函数的逆变,可以得到

    (k: "foo") => void  <=  (k: "foo" & 42 & true) => void
    (k: 42) => void     <=  (k: "foo" & 42 & true) => void
    (k: true) => void   <=  (k: "foo" & 42 & true) => void
  3. 最后类型推导的结果是 "foo" & 42 & true

其实看到最后的结果是有点懵的,为什么就是交叉类型呢?

把它想象成函数的重载,就好理解了

function fn(k: "foo" & 42 & true): void;
function fn(k: "foo"): void {}
// 42 和 true 同样的道理

传入什么类型可以保证 "foo" 类型安全呢?

一定是 "foo" 的子类型

"foo" 的子类型是什么呢?

never

如何得到 never 呢?

"foo" & 42 & true

TIP

要明确一个概念交叉类型 IntersectionType 是交集,联合类型 UnionType 是并集

所以 string & number & boolean < string < string | number | boolean

参考文章

  1. 大白话聊 TypeScript 中的变型
  2. 聊聊TypeScript类型兼容,协变、逆变、双向协变以及不变性,评论区这段话很受启发:

    狗是动物,所以一群狗是一群动物,这是协变。我需要一张能刷人民币的信用卡,你给了我一张任意币种信用卡,这是符合要求的,即,人民币是任意货币的一种,任意币种信用卡是能刷人民币的信用卡,这个推论反过来了,叫逆变。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions