# 结构化类型系
代码语言:javascript复制class Cat {
eat() {}
}
class Dog {
eat() {}
}
function feedCat(cat: Cat) {}
feedCat(new Cat())
TypeScript 的类型系统特性:结构化类型系统。TypeScript 比较两个类型并非通过类型的名称,而是比较两个类型上实际拥有的属性与方法。Cat
与 Dog
类型上的方法是一致的,所以虽然是名字不同的类型,但仍然被视为结构一致。
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
工具类型:
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
并不成立:
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
{} extends
和 extends {}
是两种完全不同的比较方式。
{} extends object
和 {} extends Object
意味着,{}
是 object
和 Object
的字面量类型,是从类型信息层面比较,即字面量类型在基础类型之上提供了更详细的类型信息。
object extends {}
和 Object extends {}
是从结构化类型系统的比较出发,即 {}
作为一个一无所有的空对象,几乎可以被看做所有类型的基类。
对于 object extends Object
和 Object extends object
比较特殊,是基于系统设定,Object
包含了所有除了 Top Type 以外的类型(基础类型、函数类型等),object
包含了所有非原始类型的类型,即数组、对象与函数类型,这些导致了二者年中有我,我中有你的现象。
从类型信息层面出发,有:原始类型 < 原始类型对应的装箱类型 < Object 类型。
# Top Type
any
和 unknown
是系统中设定为 Top Type 的类型,是类型世界的规则产物:
type Result1 = Object extends any ? 1 : 2; // 1
type Result2 = Object extends unknown ? 1 : 2; // 1
any
代表任何可能的类型,在 any extends
时,它包含让条件成立的一部分,以及让条件不成立的一部分。
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
类型的比较也是互相成立的:
type Result1 = any extends unknown ? 1 : 2; // 1
type Result2 = unknown extends any ? 1 : 2; // 1
只关注类型信息层面的层级,结论为:Object < any / unknown
# Bottom Type
never
类型,代表“虚无”的类型,一个不存在的类型。
never
类型是任何类型的子类型,包括字面量类型:
type Result = never extends 'Cell' ? 1 : 2; // 1
需要注意的是,在 TypeScript 中, void
、undefined
、null
都是切实存在、有实际意义的类型,和 string
、number
、object
并没有本质区别:
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 < 字面量类型
# 其他比较场景
对于基类和派生类
- 通常情况下派生类会完全保留基类的结果,而只是自己新增新的属性或方法
- 在结构化类型比较下,派生类类型自然会存在子类型关系
联合类型
- 只需要比较一个联合类型是否可以被视为另一个联合类型的子类型
- 即联合类型中的每个成员在另一个联合类型中都存在对应的成员
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
关键字来在条件类型中提取类型的某一部分信息:
// 当传入的类型满足 T extends (...args: any[]) => infer R 时,
// 返回 infer R 位置的值(即 R),否则返回 never
type FunctionReturnType<T extends Func> = T extends (
...args: any[]
) => infer R
? R
: never;
infer
是 inference
的缩写,意为“推断”。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
:
// 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 中主要是 Pick
和 Omit
:
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
会将传入的联合类型作为需要保留的属性:
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];
// };
Omit
是 Pick
的反向实现,Pick
保留传入的键,Omit
则是移除传入的键:
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;
具体实现其实就是条件类型的分布式特性,即当 T
、U
都是联合类型时,T
的成员会依次被拿出来进行 extends U ? T1 : T2
计算,然后将最终结果合并为一个联合类型。
交集 Extract
运行逻辑:
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
运行逻辑:
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
进行模式匹配的工具类型:
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
:
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;
对于函数类型比较,实际上要比较的是参数类型和返回值类型。
对于 Animal
、Dog
、Corgi
三个类,如果将他们分别可重复地放置在参数类型与返回值类型,可以得到下面签名函数
Animal => Animal
Animal => Dog
Animal => Corgi
Dog => Dog
Dog => Animal
Dog => Corgi
Corgi => Corgi
Corgi => Animal
Corgi => Dog
// 如果一个值能被赋值给某个类型的变量,可以认为这个值的类型为变量类型的子类型
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
在比较两个函数类型是否兼容时,将会对函数参数进行更严格的检查,即对函数参数类型启动逆变检查。
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
时,默认情况,对函数参数的检查采用双变,即逆变与协变都被认为是可以接受的。