前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TS 进阶 - 类型系统

TS 进阶 - 类型系统

作者头像
Cellinlab
发布2023-05-17 20:21:03
1.1K0
发布2023-05-17 20:21:03
举报
文章被收录于专栏:Cellinlab's Blog

# 结构化类型系

代码语言: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 时,默认情况,对函数参数的检查采用双变,即逆变与协变都被认为是可以接受的。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/8/4,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 结构化类型系
  • # 标称类型系统
    • # TypeScript 中模拟标称类型系统
      • # 类型、类型系统与类型检查
      • # 类型系统层级
        • # 判断类型兼容性的方式
          • # 原始类型
            • # 联合类型
              • # 装箱类型
                • # Top Type
                  • # Bottom Type
                    • # 其他比较场景
                    • # 类型里的逻辑运算
                      • # 条件类型
                        • # infer
                          • # 分布式条件类型
                            • # IsAny 与 IsUnknown
                            • # 内置工具类型
                              • # 内置工具类型分类
                                • # 属性修饰工具类型
                                  • # 结构工具类型
                                    • # 集合工具类型
                                      • # 模式匹配工具类型
                                      • # 上下文类型
                                        • # void 返回值类型下的特殊情况
                                        • # 逆变与协变
                                          • # 如何比较函数的签名类型
                                            • # 协变与逆变
                                              • # StrictFunctionTypes
                                              领券
                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档