TypeScript 类型父子级、逆变、协变、双向协变和不变

2023 年 2 月 3 日 星期五(已编辑)
/ ,
525
1
AI 生成的摘要
这篇文章上次修改于 2023 年 5 月 11 日 星期四,可能部分内容已经不适用,如有疑问可询问作者。

TypeScript 类型父子级、逆变、协变、双向协变和不变

我也是最近刚接触到了这些知识,文章可能有些错误,希望大佬多多指点(

对于学习 TypeScript 了解类型的逆变、协变、双向协变和不变是很重要的,但你只要明白类型的父子级关系,这些概念理解起来就会容易许多,因此在讲述这些之前我们必须先学会类型的父子级关系。

类型的父子级

首先明确一个概念,对于 TypeScript 而言,只要类型结构上是一致的,那么就可以确定父子关系,这点与 Java 是不一样的(Java 必须通过 extends 才算继承)。

我们可以看下面的例子:

interface Person {
    name: string;
    age: number;
} 

interface Suemor {
    name: string;
    age: number;
    hobbies: string[]
}

你应该可以发现这两个类型是有继承关系,此时你可以去思考到底谁是父级、谁是子级?

你可能会觉得 Suemor 是 Person 的父类型(毕竟 Person 有 2 个属性,而 Suemor 有 3 个属性且包含 Person),如果是这么理解的话那就错。

在类型系统中,属性更多的类型是子类型,也就是说 Suemor 是 Person 的子类型

因为这是反直觉的,你可能很难理解(我当时也理解不了),你可以尝试这样去理解:因为 A extends B , 于是 A 就可以去扩展 B 的属性,那么 A 的属性往往会比 B 更多,因此 A 就是子类型。或者你记住一个特征,子类型比父类型更加具体

另外判断联合类型父子关系的时候, 'a' | 'b' 和 'a' | 'b' | 'c' 哪个更具体?

'a' | 'b' 更具体,所以 'a' | 'b' 是 'a' | 'b' | 'c' 的子类型。

协变

对象中运用

协变理解起来很简单,你可能在平日里开发经常用到,例如:

interface Person {
    name: string;
    age: number;
} 

interface Suemor {
    name: string;
    age: number;
    hobbies: string[]
}

let person: Person = { // 父级
    name: '',
    age: 20
};
let suemor: Suemor = { // 子级
    name: 'suemor',
    age: 20,
    hobbies: ['play game', 'codeing']
};

//正确
person = suemor;
//报错,如果你的编辑器没有报错,请打开严格模式,至于为什么后面双向协变会讲
suemor = person;

这俩类型不一样,但是 suemor 却可以赋值给 person,也就是子级可以赋值给父级,反之不行(至于为什么,你可以想想假如 person 能够正确赋值给 suemor,那么调用 suemor.hobbies你的程序就坏到了)。

因此得出结论: 子类型可以赋值给父类型的情况就叫做协变。

函数中运用

同样的函数中也可以用到协变,例如:

interface Person { 
  name: string;
  age: number;
}

function fn(person: Person) {} // 父级

const suemor = { // 子级
  name: "suemor",
  age: 19,
  hobbies: ["play game", "codeing"],
};

fn(suemor);

fn({
  name: "suemor",
  age: 19,
  // 报错
  // 这里补充个知识点(因为当时我学的时候脑抽了),这里的 hobbies 会报错,是因为它是直接赋值,并没有类型推导。
  hobbies: ["play game", "codeing"] 
})

这里我们多给一个 hobbies,同理因为协变,子类型可以赋值给父类型。

因此我们平日的redux,在声明 dispatch 类型的时候,可以这样去写:

interface Action {
  type: string;
}

function dispatch<T extends Action>(action: T) {

}

dispatch({
  type: "suemor",
  text:'测试'
});

这样约束了传入的参数一定是 Action 的子类型,也就是说必须有 type,其他的属性有没有都可以。

双向协变

我们再看一下上上节的例子:

interface Person {
    name: string;
    age: number;
} 

interface Suemor {
    name: string;
    age: number;
    hobbies: string[]
}

let person: Person = { // 父级
    name: '',
    age: 20
};
let suemor: Suemor = { // 子级
    name: 'suemor',
    age: 20,
    hobbies: ['play game', 'codeing']
};

//正确
person = suemor;
//报错 -> 设置双向协变可以避免报错
suemor = person;

suemor = person的报错我们可以在 tsconfig.json设置 strictFunctionTypes:false或者关闭严格模式,此时我们父类型可以赋值给子类型,子类型可以赋值给父类型,这种情况我们便称为双向协变

因此双向协变就是: 父类型可以赋值给子类型,子类型可以赋值给父类型

但是这明显是有问题的,不能保证类型安全,因此我们一般都会打开严格模式,避免出现双向协变。

不变

不变是最简单的。如果没有继承关系(A 和 B 没有一方包含对方全部属性)那它就是不变,因此非父子类型之间只要类型不一样就会报错:

interface Person {
  name: string;
  age: number;
}

interface Suemor {
  name: string;
  sex:boolean
}

let person: Person = {
  name: "",
  age: 20,
};

let suemor: Suemor = {
  name: 'suemor',
  sex:true
};

// 报错
person = suemor;

逆变

逆变相对难理解一点,看下方例子:

let fn1: (a: string, b: number) => void = (a, b) => {
  console.log(a);
};
let fn2: (a: string, b: number, c: boolean) => void = (a, b, c) => {
  console.log(c);
};

fn1 = fn2; // 报错
fn2 = fn1; // 这样可以

你会发现:fn1 的参数是 fn2 的参数的父类型,那为啥能赋值给子类型?

这就是逆变,父类型可以赋值给子类型,函数的参数有逆变的性质(而返回值是协变的,也就是子类型可以赋值给父类型)。

至于为什么,如果fn1 = fn2是正确的话,我们只能传入fn1('suemor',123),但 fn1调却要输出 c,那就坏掉了。

因此我感觉逆变一般会出现在: 父函数参数与子函数参数之间赋值的时候(注意是函数与函数之间,而不是调用函数的时候,我是这么理解的,不知道对不对)。

因为逆变相对在类型做运算时用的会多一点,因此我们再看一个稍微难一点例子:

// 提取返回值类型
type GetReturnType<Func extends Function> = Func extends (
  ...args: unknown[]
) => infer ReturnType
  ? ReturnType
  : never;

type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;
image-20230203205737963

image-20230203205737963

这里GetReturnType使用来提取返回值类型,这里ReturnTypeResullt原本应当是suemor,但如上代码却得出结果为never

因为函数参数遵循逆变,也就是只能父类型赋值给子类型,但很明显这里的 unknown{name: string} 的父类型,所以反了,应该把unknown改为string的子类型才行,所以应该把 unknown 改为any或者never,如下为正确答案:

type GetReturnType<Func extends Function> = Func extends (
  ...args: any[]
) => infer ReturnType
  ? ReturnType
  : never;

type ReturnTypeResullt = GetReturnType<(name: string) => "suemor">;
image-20230203205711934

image-20230203205711934
  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...