TS 进阶 - 类型系统

2023-05-17 20:21:03 浏览数 (4)

# 结构化类型系

代码语言:javascript复制
class Cat {
  eat() {}
}

class Dog {
  eat() {}
}

function feedCat(cat: Cat) {}

feedCat(new Cat())

TypeScript 的类型系统特性:结构化类型系统。TypeScript 比较两个类型并非通过类型的名称,而是比较两个类型上实际拥有的属性与方法。CatDog 类型上的方法是一致的,所以虽然是名字不同的类型,但仍然被视为结构一致。

代码语言:javascript复制
class Cat {
  meow() {}
  eat() {}
}

class Dog {
  eat() {}
}

function feedCat(cat: Cat) {}

feedCat(new Dog()) // Error
// Argument of type 'Dog' is not assignable to parameter of type 'Cat'.
// Property 'meow' is missing in type 'Dog' but required in type 'Cat'.

结构类型的别称鸭子类型,来源于鸭子测试,其核心思想是:如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子。

代码语言:javascript复制
class Cat {
  eat() {}
}

class Dog {
  bark() {}
  eat() {}
}

function feedCat(cat: Cat) {}

feedCat(new Dog()) // OK

结构化类型系统认为 Dog 类型完全实现了 Cat 类型,至于额外的 bark ,可以认为是 Dog 类型继承 Cat 类型后添加的新方法,即此时 Dog 类可以被认为是 Cat 类的子类。

在比较对象类型的属性时,同样会采用结构化类型系统进行判断。而对结构中的函数类型(即方法)进行比较时,同样存在类型的兼容性比较:

代码语言:javascript复制
class Cat {
  eat(): boolean {
    return true
  }
}

class Dog {
  eat(): number {
    return 599
  }
}

function feedCat(cat: Cat) {}

feedCat(new Dog()) // Error
// Argument of type 'Dog' is not assignable to parameter of type 'Cat'.
// The types returned by 'eat()' are incompatible between these types.
// Type 'number' is not assignable to type 'boolean'.

结构化类型系统的核心系统的核心理念:基于类型结构进行判断类型兼容性。严格讲,鸭子类型系统和结构化类型系统并不完全一致,结构化类型系统基于完全的类型结构来判断类型兼容性,而鸭子类型只基于运行时访问的部分决定。

# 标称类型系统

标称类型系统,是基于类型名进行兼容性判断的类型系统,要求两个可兼容的类型,其名称必须完全一致

代码语言:javascript复制
type USD = number;
type CNY = number;

const CNYCount: CNY = 100;
const USDCount: USD = 100;

function addCNY(source: CNY, input: CNY) {
  return source   input;
}

addCNY(CNYCount, USDCount) // OK

在结构化类型系统中,USD 与 CNY 被认为是两个完全一致的类型。

在标称类型系统中,USD 与 CNY 被认为是两个不同的类型,因此在进行类型兼容性判断时,会报错。

# TypeScript 中模拟标称类型系统

类型的重要意义之一是限制了数据的可用操作与意义。这往往是通过类型附带的额外信息来实现(类似于元数据),要在 TypeScript 中实现,只需要为类型额外附加元数据即可,如 CNY 与 USD ,分别附加上其单位信息即可,但同时又需要保留原本的信息(即原本的 number 类型):

通过交叉类型的方式来实现信息的附加:

代码语言:javascript复制
export declare class TagProtector<T extends string> {
  protected __tag__: T;
}

export type Nominal<T, U extends string> = T & TagProtector<U>;

使用 TagProtector 声明了一个具有 protected 属性的类,使用它来携带额外的信息,并和原本的类型合并到一起,就得到 Nominal 工具类型:

代码语言:javascript复制
export type CNY = Nominal<number, 'CNY'>;

export type USD = Nominal<number, 'USD'>;

const CNYCount = 100 as CNY;

const USDCount = 100 as USD;

function addCNY(source: CNY, input: CNY) {
  return (source   input) as CNY;
}

addCNY(CNYCount, CNYCount); // OK

addCNY(CNYCount, USDCount); // Error

除了在类型层面做处理,还可以在运行时进行进一步限制,通过从逻辑层处理:

代码语言:javascript复制
class CNY {
  private __tag!: void;
  constructor(public value: number) {}
}

class USD {
  private __tag!: void;
  constructor(public value: number) {}
}

const CNYCount = new CNY(100);
const USDCount = new USD(100);

function addCNY(source: CNY, input: CNY) {
  return (source.value   input.value);
}

addCNY(CNYCount, CNYCount); // OK
addCNY(CNYCount, USDCount); // Error

# 类型、类型系统与类型检查

  • 类型
    • 限制数据的可用操作、意义、允许的值的集合,即访问限制赋值限制
    • 在 TypeScript 中即原始类型、对象类型、函数类型、字面量类型等基础类型,以及类型别名、联合类型等经过类型编程后得到的类型
  • 类型系统
    • 一组为变量、函数等结构分配、实施类型的规则,通过显式地指定或类型推导来分配类型
    • 同时类型系统定义了如何判断类型之间的兼容性:在 TypeScript 中即结构化类型系统
  • 类型检查
    • 确保类型遵循类型系统下的类型兼容性
    • 对于静态类型语言,在编译时进行,对于动态语言,在运行时检查

静态类型与动态类型指的是类型检查发生的时机,并不等于这门语言的类型能力。

# 类型系统层级

类型层级指,TypeScript 中所有类型的兼容关系,从最上面一层的 any 类型,到最底层的 never 类型

# 判断类型兼容性的方式

使用条件类型来判断类型兼容性:

代码语言:javascript复制
// 如果返回 1 ,说明 `Cell` 是 string 的子类型
type Result = 'Cell' extends string ? 1 : 2;

通过赋值来进行兼容性检查:

代码语言:javascript复制
// 如果 变量a = 变量b 成立,意味 <变量b的类型> extends <变量a的类型> 成立
declare let source: string;

declare let anyType: any;
declare let neverType: never;

anyType = source; // OK

neverType = source; // Error

# 原始类型

代码语言:javascript复制
type Result1 = 'Cell' extends string ? 1 : 2; // 1
type Result2 = 2022 extends number ? 1 : 2; // 1
type Result3 = true extends boolean ? 1 : 2; // 1
type Result4 = { name: string } extends object ? 1 : 2; // 1
type Result5 = { name: 'Cell' } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1

一个基础类型和它们对应的字面量类型必定存在父子类型关系。严格讲,object 实际上代表所有非原始类型的类型,即数组、对象与函数类型

  • 字面量类型 < 对应的原始类型

# 联合类型

在联合类型中,只需要符合其中一个类型,就可以认为实现了这个联合类型,用条件类型表达是:

代码语言:javascript复制
type Result7 = 1 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result8 = 'Cell' extends 'Cell' | 'Cellinlab' ? 1 : 2; // 1
type Result9 = true extends true | false ? 1 : 2; // 1

并不需要联合类型的所有成员均为字面量类型,或者字面量类型来自于同一基础类型,只需要该类型存在于联合类型中。

对于原始类型,联合类型的比较也是一致的:

代码语言:javascript复制
type Result10 = string extends string | false | number ? 1 : 2; // 1

  • 字面量类型 < 包含包含该字面量类型的联合类型
  • 原始类型 < 包含该原始类型的联合类型

如果一个联合类型由同一个基础类型的类型字面量组成:

代码语言:javascript复制
type Result11 = 'Cell' | 'Cellinlab' | 'linlan' extends string ? 1 : 2; // 1
type Result12 = {} | {() => void} | [] extends object ? 1 : 2; // 1

  • 同一基础类型的字面量联合类型 < 此基础类型
  • 字面量类型 < 包含此字面量类型的联合类型(同一基础类型)< 对应的原始类型

# 装箱类型

代码语言:javascript复制
type Result1 = string extends String ? 1 : 2; // 1
type Result2 = String extends Object ? 1 : 2; // 1
type Result3 = {} extends object ? 1 : 2; // 1
type Result4 = object extends Object ? 1 : 2; // 1

在结构化类型系统的比较下,String 会被认为是 {} 的子类型。看起来存在 string < {} < object 类型链,但实际上 string extends object 并不成立:

代码语言:javascript复制
type Tmp = string extends object ? 1 : 2; // 2

由于结构化类型系统特性的存在,会看到一些看起来矛盾的现象:

代码语言:javascript复制
type Result1 = {} extends object ? 1 : 2; // 1
type Result2 = object extends {} ? 1 : 2; // 1

type Result3 = object extends Object ? 1 : 2; // 1
type Result4 = Object extends object ? 1 : 2; // 1

type Result5 = {} extends Object ? 1 : 2; // 1
type Result6 = Object extends {} ? 1 : 2; // 1

{} extendsextends {} 是两种完全不同的比较方式。

{} extends object{} extends Object 意味着,{}objectObject 的字面量类型,是从类型信息层面比较,即字面量类型在基础类型之上提供了更详细的类型信息。

object extends {}Object extends {} 是从结构化类型系统的比较出发,即 {} 作为一个一无所有的空对象,几乎可以被看做所有类型的基类。

对于 object extends ObjectObject extends object 比较特殊,是基于系统设定,Object 包含了所有除了 Top Type 以外的类型(基础类型、函数类型等),object 包含了所有非原始类型的类型,即数组、对象与函数类型,这些导致了二者年中有我,我中有你的现象。

从类型信息层面出发,有:原始类型 < 原始类型对应的装箱类型 < Object 类型

# Top Type

anyunknown 是系统中设定为 Top Type 的类型,是类型世界的规则产物:

代码语言:javascript复制
type Result1 = Object extends any ? 1 : 2; // 1
type Result2 = Object extends unknown ? 1 : 2; // 1

any 代表任何可能的类型,在 any extends 时,它包含让条件成立的一部分,以及让条件不成立的一部分。

代码语言:javascript复制
type Resutl1 = any extends Object ? 1 : 2; // 1 | 2
type Result2 = any extends string ? 1 : 2; // 1 | 2
type Result3 = any extends 'Cell' ? 1 : 2; // 1 | 2
type Result4 = any extends never ? 1 : 2; // 1 | 2

在 TypeScript 内部代码的条件类型处理中,如果接受判断的是 any ,那么会直接返回条件类型结果组成的联合类型。所以此处的 any 是带限定条件的。

any 类型和 unknown 类型的比较也是互相成立的:

代码语言:javascript复制
type Result1 = any extends unknown ? 1 : 2; // 1
type Result2 = unknown extends any ? 1 : 2; // 1

只关注类型信息层面的层级,结论为:Object < any / unknown

# Bottom Type

never 类型,代表“虚无”的类型,一个不存在的类型。

never 类型是任何类型的子类型,包括字面量类型:

代码语言:javascript复制
type Result = never extends 'Cell' ? 1 : 2; // 1

需要注意的是,在 TypeScript 中, voidundefinednull 都是切实存在、有实际意义的类型,和 stringnumberobject 并没有本质区别:

代码语言:javascript复制
type Result1 = undefined extends 'Cell' ? 1 : 2; // 2
type Result2 = null extends 'Cell' ? 1 : 2; // 2
type Result3 = void extends 'Cell' ? 1 : 2; // 2

  • never < 字面量类型

# 其他比较场景

对于基类和派生类

  • 通常情况下派生类会完全保留基类的结果,而只是自己新增新的属性或方法
  • 在结构化类型比较下,派生类类型自然会存在子类型关系

联合类型

  • 只需要比较一个联合类型是否可以被视为另一个联合类型的子类型
  • 即联合类型中的每个成员在另一个联合类型中都存在对应的成员
代码语言:javascript复制
type Result1 = 1 | 2 | 3 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result2 = 2 | 4 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1

type Result3 = 1 | 2 | 5 extends 1 | 2 | 3 ? 1 : 2; // 2

数组和元组

代码语言:javascript复制
type Result1 = [number, number] extends number[] ? 1 : 2; // 1
type Result2 = [number, string] extends number[] ? 1 : 2; // 2
type Result3 = [number, string] extends (number | string)[] ? 1 : 2; // 1
type Result4 = [] extends number[] ? 1 : 2; // 1
type Result5 = [] extends unknown[] ? 1 : 2; // 1
type Result6 = number[] extends (number | string)[] ? 1 : 2; // 1
type Result7 = any[] extends number[] ? 1 : 2; // 1
type Result8 = unknown[] extends number[] ? 1 : 2; // 2
type Result9 = never[] extends number[] ? 1 : 2; // 1

# 类型里的逻辑运算

# 条件类型

基本语法:

代码语言:javascript复制
ValueA === ValueB ? ValueIfTrue : ValueIfFalse
TypeA extends TypeB ? ResultIfTrue : ResultIfFalse

条件类型中使用 extends 判断类型的兼容性,而非类型的全等性。在类型层面,对于能够进行赋值操作的两个变量,并不需要它们的类型完全相等,只需要具有兼容性。

条件类型绝大部分场景下会和泛型一起使用,泛型参数实际类型会在实际调用时才会被填充,而条件类型在这基础上,可以基于填充后的泛型参数做进一步的类型操作:

代码语言:javascript复制
type LiteralType<T> = T extends string ? 'string' : 'other';

type Result1 = LiteralType<'Cell'>; // 'string'
type Result2 = LiteralType<1>; // 'other'

在函数中,条件类型与泛型的搭配也很常见:

代码语言:javascript复制
function universalAdd<T extends number | bigint | string>(x: T, y: T) {
  return x   (y as any);
}

// 因为两个参数都引用了泛型参数 T,因此泛型会被填充为一个联合类型
universalAdd(1, 2); // T 填充为 1 | 2
universalAdd('Cell', '2022'); // T 填充为 Cell | 2022

# infer

TypeScript 中支持通过 infer 关键字来在条件类型中提取类型的某一部分信息

代码语言:javascript复制
// 当传入的类型满足 T extends (...args: any[]) => infer R 时,
// 返回 infer R 位置的值(即 R),否则返回 never
type FunctionReturnType<T extends Func> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

inferinference 的缩写,意为“推断”。infer R 中的 R 表示待推断的类型。infer 只能在条件类型中使用,因为实际上仍然需要类型结构时一致的。

这里的类型结构不局限于函数类型结构:

代码语言:javascript复制
type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;

type SwapResult1 = Swap<[1, 2]>; // [2, 1]
type SwapResult2 = Swap<[1, 2, 3]>; // [1, 2, 3]

// 处理任意长度元组
type ExtractStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...any[],
  infer End
]
  ? [Start, End]
  : T;

type ExtractStartAndEndResult1 = ExtractStartAndEnd<[1, 2, 3, 4]>; // [1, 4]
type ExtractStartAndEndResult2 = ExtractStartAndEnd<[1]>; // [1]

type SwapStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...infer Middle,
  infer End
]
  ? [End, ...Middle, Start]
  : T;

type SwapStartAndEndResult1 = SwapStartAndEnd<[1, 2, 3, 4]>; // [4, 2, 3, 1]

type SwapFirstTwo<T extends any[]> = T extends [infer A, infer B, ...infer C]
  ? [B, A, ...C]
  : T;

type SwapFirstTwoResult1 = SwapFirstTwo<[1, 2, 3, 4]>; // [2, 1, 3, 4]

也可以进行结构层面的转换:

代码语言:javascript复制
type ArrayItemType<T> = T extends Array<infer ElementType> ? ElementType : T;

type ArrayItemTypeResult1 = ArrayItemType<number[]>; // number
type ArrayItemTypeResult2 = ArrayItemType<number>; // number
type ArrayItemTypeResult3 = ArrayItemType<[string, number]>; // string | number

# 分布式条件类型

分布式条件类型,也称条件类型的分布式特性,是条件类型在满足一定情况下会执行的逻辑。

代码语言:javascript复制
// 是否通过泛型参数传入
type Condition<T> = T extends 1 | 2 | 3 ? T : never;

type Res1 = Condition<1 | 2 | 3 | 4 | 5>; // 1 | 2 | 3
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never; // never

// 泛型参数是否被数组包裹
type Naked<T> = T extends boolean ? 'Y' : 'N';
type Wrapped<T> = [T] extends [boolean] ? 'Y' : 'N';

type Res3 = Naked<number | boolean>; // 'N' | 'Y'
type Res4 = Wrapped<number | boolean>; // 'N'

条件类型分布式起作用的条件:

  • 类型参数需要是一个联合类型
  • 类型参数需要通过泛型参数的方式传入,不能直接进行条件类型判断
  • 条件类型中的泛型参数不能被包裹

条件类型分布式特性的作用:

  • 将联合类型拆开,每个分支分别进行一次条件类型判断,再将最后的结果合并起来
  • 或者说对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上
    • 裸类型参数指泛型参数是否完全裸露

# IsAny 与 IsUnknown

代码语言:javascript复制
type IsAny<T> = 0 extends 1 & T ? true : false;

type IsUnknown<T> = IsAny<T> extends true
  ? IsAny<T> extends false
    ? true
    : false
  : false;

# 内置工具类型

# 内置工具类型分类

  • 属性修饰工具类型
    • 对属性的修饰,包括对对象属性和数组元素的可选/必选、只读/可写
  • 结构工具类型
    • 对既有类型的裁剪、拼接、转换等
    • 如使用对一个对象类型裁剪得到一个新的对象类型,或将联合类型结构转换到交叉类型结构
  • 集合工具类型
    • 对集合(联合类型)的处理,即交集、并集、差集、补集
  • 模式匹配工具类型
    • 基于 infer 的模式匹配,即对一个既有类型特定位置类型的提取
    • 如提取函数类型签名中的返回值类型
  • 模板字符串工具类型
    • 模板字符串专属的工具类型
    • 如将一个对象类型中所有属性名转换为大驼峰形式

# 属性修饰工具类型

  • 主要使用
    • 属性修饰
    • 映射类型
    • 索引类型
      • 索引类型签名
      • 索引类型访问
      • 索引类型查询

访问性修饰工具类型:

代码语言:javascript复制
type Partial<T> = {
  [P in keyof T]?: T[P];
};
// 也可以是
// type Partial<T> = {
//   [P in keyof T] ?: T[P];
// };

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

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
// 也可以是
// type Readonly<T> = {
//    readonly [P in keyof T]: T[P];
// };

type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

注意,对于结构声明来说,一个属性是否必须提供仅取决于其是否携带可选标记,即使使用 undefined 甚至 never 也不能将其标记为可选。

# 结构工具类型

  • 主要使用
    • 条件类型
    • 映射类型
    • 索引类型

结构声明工具类型,即快速声明一个结构,如内置类型 Record

代码语言:javascript复制
// K extends keyof any 为键的类型
// T 为值类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

type Record1 = Record<string, unknown>; // { [key: string]: unknown }
type Record2 = Record<'a' | 'b', number>; // { a: number, b: number }
type Record3 = Record<string, any>; // { [key: string]: any }

在一些工具类库源码中,存在类似的结构声明工具类型,如:

代码语言:javascript复制
type Dictionary<T> = {
  [index: string]: T;
};

type NumericDictionary<T> = {
  [index: number]: T;
};

对于结构处理工具类型,在 TypeScript 中主要是 PickOmit

代码语言:javascript复制
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Pick 会将传入的联合类型作为需要保留的属性:

代码语言:javascript复制
interface Foo {
  name: string;
  age: number;
  job: JobUnionType;
}

type PickedFoo = Pick<Foo, 'name' | 'age'>; // { name: string, age: number }

// 等价于
// type Pick<T> = {
//   [P in 'name' | 'age']: T[P];
// };

OmitPick 的反向实现,Pick 保留传入的键,Omit 则是移除传入的键:

代码语言:javascript复制
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// Exclude<keyof T, K> 为 T 的键名联合类型中剔除了 K 的部分
type Tmp1 = Exclude<1, 2>; // 1
type Tmp2 = Exclude<1 | 2, 2>; // 1
type Tmp3 = Exclude<1 | 2 | 3, 2 | 3>; // 1
type Tmp4 = Exclude<1 | 2 | 3, 2 | 4>; // 1 | 3

# 集合工具类型

内置工具类型中提供了交集与差集的实现:

代码语言:javascript复制
type Extract<T, U> = T extends U ? T : never;

type Exclude<T, U> = T extends U ? never : T;

具体实现其实就是条件类型的分布式特性,即当 TU 都是联合类型时,T 的成员会依次被拿出来进行 extends U ? T1 : T2 计算,然后将最终结果合并为一个联合类型。

交集 Extract 运行逻辑:

代码语言:javascript复制
type AExtract = Extract<1 | 2 | 3, 1 | 2 | 4>; // 1 | 2

type BExtract = 
  | (1 extends 1 | 2 | 4 ? 1 : never)
  | (2 extends 1 | 2 | 4 ? 2 : never)
  | (3 extends 1 | 2 | 4 ? 3 : never); // 1 | 2

差集 Exclude 运行逻辑:

代码语言:javascript复制
type SetA = 1 | 2 | 3 | 5;
type SetB = 0 | 1 | 2 | 4;

type AExcludeB = Exclude<SetA, SetB>; // 3 | 5
type BExcludeB = Exclude<SetB, SetA>; // 0 | 4

type AExcludeB =
  | (1 extends 0 | 1 | 2 | 4 ? never : 1)
  | (2 extends 0 | 1 | 2 | 4 ? never : 2)
  | (3 extends 0 | 1 | 2 | 4 ? never : 3)
  | (5 extends 0 | 1 | 2 | 4 ? never : 5); // 3 | 5

type BExcludeA =
  | (0 extends 1 | 2 | 3 | 5 ? never : 0)
  | (1 extends 1 | 2 | 3 | 5 ? never : 1)
  | (2 extends 1 | 2 | 3 | 5 ? never : 2)
  | (4 extends 1 | 2 | 3 | 5 ? never : 4); // 0 | 4

实现并集与补集:

代码语言:javascript复制
export type MyUnion<A, B> = A | B;

export type MyIntersection<A, B> = A extends B ? A : never;

export type MyDifference<A, B> = A extends B ? never : A;

export type MyComplement<A, B extends A> = MyDifference<A, B>;

# 模式匹配工具类型

对函数类型签名的模式匹配:

代码语言:javascript复制
type FunctionType = (...args: any) => any;

type Parameters<T extends FunctionType> = T extends (...args: infer P) => any
  ? P
  : never;

type ReturnType<T extends FunctionType> = T extends (...args: any) => infer R
  ? R
  : any;

根据 infer 的位置不同,就能获取到不同位置的类型,在函数中则是参数类型与返回值类型。

还可以更精确的匹配第一个参数类型:

代码语言:javascript复制
type FirstParameter<T extends FunctionType> = T extends (
  firstArg: infer P,
  ...args: any
) => any ?
  P :
  never;

type Func1 = (arg: number) => void;
type Func2 = (arg: string, arg2: number) => void;

type FirstArg1 = FirstParameter<Func1>; // number
type FirstArg2 = FirstParameter<Func2>; // string

内置工具类型中还有一组对 Class 进行模式匹配的工具类型:

代码语言:javascript复制
type classType = abstract new (...args: any) => any;

type ConstructorParameters<T extends classType> = T extends abstract new (
  ...args: infer P
) => any
  ? P
  : never;

type InstanceType<T extends classType> = T extends abstract new (
  ...args: any
) => infer R
  ? R
  : any;

infer 约束

代码语言:javascript复制
type FirstArrayItemType<T extends any[]> = T extedns [infer P extedns string, ...any[]]
  ? P
  : never;

type Tmp1 = FirstArrayItemType<[2022, 'Cell']>; // never
type Tmp2 = FirstArrayItemType<['Cell', 2022]>; // string
type Tmp3 = FirstArrayItemType<['Cellinlab']>; // string

# 上下文类型

TypeScript 的类型推导除了依赖开发者的输入,如变量声明、函数逻辑、类型保护等。还存在另一种类型推导,即上下文类型推导。

上下文类型的核心理念:基于位置的类型推导。相对于基于开发者输入进行的类型推导,上下文类型更像是反方向的类型推导,基于已定义的类型来规范开发者的使用。

# void 返回值类型下的特殊情况

代码语言:javascript复制
type CustomHandler = (name: string, age: number) => void;

const handler1: CustomHandler = (name, age) => true;
const handler2: CustomHandler = (name, age) => string;
const handler3: CustomHandler = (name, age) => null;
const handler4: CustomHandler = (name, age) => undefined;

上下文类型对于 void 返回值类型的函数,并不会要求其什么都不能返回。虽然这些函数实现可以返回任意类型的值,但对于调用结果的类型,仍是 void:

代码语言:javascript复制
const result1 = handler1("Cellin", 18); // void
const result2 = handler2("Cellin", 18); // void
const result3 = handler3("Cellin", 18); // void
const result4 = handler4("Cellin", 18); // void

对于一个 void 类型的函数,不会去消费其返回值,因此对于返回值的类型,不会有任何要求。

# 逆变与协变

# 如何比较函数的签名类型

代码语言:javascript复制
class Animal {
  asPet() {}
}

class Dog extedns Animal {
  bark() {}
}

class Corgi extedns Dog {
  play() {}
}

const DogFactory = (args: Dog) => Dog;

对于函数类型比较,实际上要比较的是参数类型和返回值类型。

对于 AnimalDogCorgi 三个类,如果将他们分别可重复地放置在参数类型与返回值类型,可以得到下面签名函数

  • Animal => Animal
  • Animal => Dog
  • Animal => Corgi
  • Dog => Dog
  • Dog => Animal
  • Dog => Corgi
  • Corgi => Corgi
  • Corgi => Animal
  • Corgi => Dog
代码语言:javascript复制
// 如果一个值能被赋值给某个类型的变量,可以认为这个值的类型为变量类型的子类型
function makeDogBark(dog: Dog) {
  dog.bark();
}

// 派生类会保留基类的属性和方法,所以与基类兼容
makeDogBark(new Corgi()); // ok
makeDogBark(new Dog()); // ok
makeDogBark(new Animal()); // error

这里通过将具有父子关系的类型放置在参数位置以及返回值位置上,最终函数类型的关系直接取决于类型的父子关系。

# 协变与逆变

随着某一量的变化,随之变化一致的为协变,变化相反的为逆变。

用 TypeScript 思路进行转换,如果有 A << B,协变意味着 Wrapper<A> << Wrapper<B>,逆变意味着 Wrapper<B> << Wrapper<A>

示例中,变化即从单个类型到函数类型的包装过程:

代码语言:javascript复制
type AsFuncArgType<T> = (arg: T) => void;
type AsFuncReturnType<T> = (arg: unknown) => T;

type CheckReturnType = AsFuncReturnType<Corgi> extends AsFuncReturnType<Dog>
  ? true
  : false; // true

type CheckArgType = AsFuncArgType<Dog> extends AsFuncArgType<Animal>
  ? true
  : false; // false

函数类型的参数类型使用子类型逆变的方式确定是否成立,返回值类型使用子类型协变的方式确定是否成立。

# StrictFunctionTypes

StrictFunctionTypes 在比较两个函数类型是否兼容时,将会对函数参数进行更严格的检查,即对函数参数类型启动逆变检查。

代码语言:javascript复制
function fn(dog: Dog) {
  dog.bark();
}

type CorgiFunc = (corgi: Corgi) => void;
type AnimalFunc = (animal: Animal) => void;

const corgiFunc: CorgiFunc = fn; // ok
const animalFunc: AnimalFunc = fn; // error

在不开启 StrictFunctionTypes 时,默认情况,对函数参数的检查采用双变,即逆变与协变都被认为是可以接受的。

1 人点赞