Typescript 应用篇

90 阅读6分钟

一、子类型兼容性

子类型与超类型都有其各自的数据类型,将两者关联在一起的是它们之间的可替换关系。面向对象程序设计中的里氏替换原则描述了程序中任何使用了超类型的地方都可以用其子类型进行替换,并且在替换后程序的行为保持不变。

概括:

使用超类型的地方都可以用其子类型替换,而不影响程序执行。

基本性质

若类型A是类型B的子类型,则记作:

A <: B

若类型B是类型A的子类型,则记作:

A :> B

自反性:

A <: A 且 A >: A

传递性:

A <: B <: C,则 A <: C

原始类型

顶端类型:所有类型都是顶端类型的子类型

any、unknown

// 可以互相赋值
declare let s: any;
let t: unknown = s;

declare let s: unknown;
let t: any = s;

原始类型:

number、bigint、boolean、string、symbol、void、null、undefined、枚举类型、字面量类型

// 除顶端类型外,void、null、枚举、字面量的值只能被同类型的值替换
/** 枚举间不能相互赋值 */
enum E {
  A,
  B,
}
enum F {
  A,
  B,
  C,
}
declare let s: E;
let t: F = s; // Type 'E' is not assignable to type 'F'.

/** 字面量间不能相互赋值 */
declare let s: 1;
let t: 2 = s; // Type '1' is not assignable to type '2'.

declare let s: undefined;
let t: void = s; // 正确

尾端类型:是所有类型的子类型

never

对象类型

仅通过比较两个对象类型的成员就能够确定它们的子类型关系。

对于属性对象而言只需比较两个对象类型的必选属性

父类型的所有属性成员都能在子类型中找到(特指二者必选的属性成员),且相对应的属性成员类型父子关系也要成立

// T 是 S 的子类型
interface S {
    x: string;
    y: string;
    w?: number;
}

interface T {
    x: 'x';
    y: 'y';
    z: 'z';
}

declare let s: S;
declare let t: T;

s = t;

注意:这里的赋值兼容性与类型实例化赋值不是一个概念

interface S {
    x: string;
    y: string;
    z: number;
}

let s: S = {
    x: '',
    y: '',
    z: 3,
    w: 0,
//  ~~~~ 'w' does not exist in type 'S'.
};

联合类型

联合类型实例化赋值

interface S0 {
    area: number;
    radius: number;
    other?: string;
}

interface S1 {
    area: string;
    width: number;
    height: number;
    another?: boolean;
}

type S = S0 | S1;

// S 类型只有公共必选属性
declare const s: S;
s.area;
s.radius; // 错误,S 不存在属性 radius
s.width;  // 错误,S 不存在属性 width
s.height; // 错误,S 不存在属性 height
s.other; // 错误,S 不存在属性 other
s.another; // 错误,S 不存在属性 another

/************************************************/
/* 赋值的类型需要满足至少包含 S0 或 S1 的所有必选属性  */
/************************************************/

/**
 * 如果要满足 S0,
 * 则 area 和 radius 必须存在,其他属性(S0 或 S1 范围内的非公共属性)可选
 */
const s0: S = {
    area: 1,
    radius: 1,
};

/**
 * 同理,如果要满足 S1,
 * 则 area、width 和 height 必须存在,其他属性(S0 或 S1 范围内的非公共属性)可选
 */
const s1: S = {
    area: '1',
    width: 1,
    height: 2,
};

联合类型间的子类型关系

假设有联合类型 “S = S0 | S1” 和任意类型 T

如果 T <: S0 或 T <: S1,那么 T <: S

如果 S0 <: T 且 S1 <: T,那么 S <: T

interface S0 {
    area: number;
    radius: number;
    other?: string;
}
interface S1 {
    area: number;
    width: number;
    height: number;
    another?: boolean;
}
type S = S0 | S1;

// 1. 声明 S 类型的变量 s
declare let s: S;

// 2. 定义子类型 T,满足 T <: S0 或 T <: S1
type T0 = {
    area: number;
    radius: number;
    x: boolean;
}
type T1 = {
    area: number;
    width: number;
    height: number;
    x: boolean;
}
// 3. 声明 T 类型的变量 t
declare let t0: T0;
declare let t1: T1;

s = t0;
s = t1; // t 有多余属性 x 也可以赋值,即子类型 T 的值可以替换 父类型 S 的值

交叉类型

交叉类型实例化赋值

interface S0 {
    area: number;
    radius: number;
    other?: string;
}

interface S1 {
    area: number;
    width: number;
    height: number;
    another?: boolean;
}

type S = S0 & S1;

// S 类型包含所有属性
declare const s: S;
s.area;
s.radius;
s.width;
s.height;
s.other;
s.another;

/************************************************/
/* 赋值的类型需要满足至少包含 S0 和 S1 的所有必选属性  */
/************************************************/

/**
 * S0 和 S1 的必选属性必须存在,其他属性(S0 或 S1 的可选属性)可选
 */
const s0: S = {
    area: 1,
    radius: 1,
    width: 1,
    height: 2,
};

交叉类型间的子类型关系

假设有联合类型 “S = S0 & S1” 和任意类型 T

如果 T <: S0 且 T <: S1,那么 T <: S

如果 S0 <: T 或 S1 <: T,那么 S <: T

interface S0 {
    area: number;
    radius: number;
    other?: string;
}
interface S1 {
    area: number;
    width: number;
    height: number;
    another?: boolean;
}
type S = S0 & S1;

// 1. 声明 S 类型的变量 s
declare let s: S;

// 2. 定义子类型 T,满足 T <: S0 且 T <: S1
type T = {
    area: number;
    radius: number;
    width: number;
    height: number;
    x: boolean;
}
// 3. 声明 T 类型的变量 t
declare let t: T;

s = t; // t 有多余属性 x 也可以赋值,即子类型 T 的值可以替换 父类型 S 的值

函数类型

在介绍函数类型间的子类型关系之前,让我们先介绍一个重要的概念——变型

变形的概念

如果复杂类型Complex是由类型T构成,那么我们将其记作Complex(T)

协变

如果由 A 是 B 的子类型能够得出 Complex(A) 是 Complex(B) 的子类型,那么我们将这种变型称作协变

A <: B → Complex(A) <: Complex(B)

逆变

如果由 A 是 B 的子类型能够得出 Complex(A) 是 Complex(A) 的子类型,那么我们将这种变型称作逆变

A <: B → Complex(B) <: Complex(A)

双变

如果由A是B的子类型或者B是A的子类型能够得出Complex(A)是Complex(B)的子类型,那么我们将这种变型称作双变

A <: B 或 B <: A → Complex(A) <: Complex(B)

不变

若类型间不存在上述变型关系,那么我们称之为不变

函数类型间的子类型关系判断

逆变关系的函数参数

  1. 参数个数相同

若函数类型 S 是函数类型 T 的子类型,那么 S 的参数类型必须是 T 中对应参数类型的超类型

// 因 a <: x,故 S :> T
type S = (a: 0 | 1) => void;
type T = (x: number) => void;

declare let s: S;
declare let t: T;
// 子类值 t 可以替换父类值 s
s = t;
  1. 参数个数不同

若函数类型S是函数类型T的子类型,则S中的每一个必选参数必须能够在T中找到对应的参数,即S中必选参数的个数不能多于T中的参数个数

即:子类型函数必选参数要少(当然也要满足对应的参数都是超类型)

// 因 S 的必选参数在 T 中都能找到,故 S <: T
type S = (a: number) => void;
type T = (x: number, y: number) => void;
// type T = (x: 1, y: number) => void;

declare let s: S;
declare let t: T;
// 子类的值 s 可以替换父类的值 t
t = s;

协变关系的函数返回值

若函数类型S是函数类型T的子类型,那么S的返回值类型必须是T的返回值类型的子类型

// 因 0 | 1 是 number 子类型,故 S 是 T 类型的子类型
type S = () => 0 | 1;
type T = () => number;

declare let s: S;
declare let t: T;
// 子类的值 s 可以替换父类的值 t
t = s;