type 与 interface
interface 用来描述对象、类的结构,而类型别名用来将一个函数签名、一组联合类型、一个工具类型等等 抽离成一个完整独立的类型。
object、Object 以及 { }
在 TypeScript 中,Object 包含了所有的类型:
// 对于 undefined、null、void 0 ,需要关闭 strictNullChecks
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;
const tmp4: Object = 'linbudu';
const tmp5: Object = 599;
const tmp6: Object = { name: 'linbudu' };
const tmp7: Object = () => {};
const tmp8: Object = [];
Object、Boolean、Number、String、Symbol,这几个装箱类型(Boxed Types) 同样包含了一些超出预期的 类型。以 String 为例,它同样包括 undefined、null、void,以及代表的 拆箱类型(Unboxed Types) string。
const tmp9: String = undefined;
const tmp10: String = null;
const tmp11: String = void 0;
const tmp12: String = 'linbudu';
在任何情况下,你都不应该使用这些装箱类型。
object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数 类型。
const tmp22: object = { name: 'linbudu' };
const tmp23: object = () => {};
const tmp24: object = [];
const tmp25: object = {};
{}
作为类型签名就是一个合法的,但内部无属性定义的空对象,这类似于 Object(想想 new Object()
)
,它意味着任何非 null / undefined 的值:
const tmp25: {} = undefined; // 仅在关闭 strictNullChecks 时成立,下同
const tmp26: {} = null;
const tmp27: {} = void 0; // void 0 等价于 undefined
const tmp28: {} = 'linbudu';
const tmp29: {} = 599;
const tmp30: {} = { name: 'linbudu' };
const tmp31: {} = () => {};
const tmp32: {} = [];
虽然能够将其作为变量的类型,但你实际上无法对这个变量进行任何赋值操作:
const tmp30: {} = { name: 'linbudu' };
tmp30.age = 18; // X 类型“{}”上不存在属性“age”。
为了更好地区分 Object
、object
以及{}
这三个具有迷惑性的类型,我们再做下总结:
- 在任何时候都不要使用 Object 以及类似的装箱类型。
- 当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用 object。
- 进一步区分,也就是使用
Record<string, unknown>
或Record<string, any>
表示对象 unknown[]
或any[]
表示数组(...args: any[]) => any
表示函数这样
- 进一步区分,也就是使用
- 避免使用
{}
。{}
意味着任何非null / undefined
的值,它和any
一样恶劣。
枚举
enum Items {
Foo,
Bar = 'BarValue',
Baz = 'BazValue'
}
// 编译结果,只会进行 键-值 的单向映射
('use strict');
var Items;
(function (Items) {
Items[(Items['Foo'] = 0)] = 'Foo';
Items['Bar'] = 'BarValue';
Items['Baz'] = 'BazValue';
})(Items || (Items = {}));
函数
function foo(arg1: string, ...rest: [number, boolean]) {}
foo('linbudu', 18, true);
重载
function func(foo: number, bar: true): string; // 重载签名一
function func(foo: number, bar?: false): number; // 重载签名二
// 函数的实现签名,会包含重载签名的所有可能情况。
function func(foo: number, bar?: boolean): string | number {
if (bar) {
return String(foo);
} else {
return foo * 599;
}
}
const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number
异步函数、Generator 函数等类型签名
async function asyncFunc(): Promise<void> {}
function* genFunc(): Iterable<void> {}
async function* asyncGenFunc(): AsyncIterable<void> {}
class
class Foo {
private prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
protected print(addon: string): void {
console.log(`${this.prop} and ${addon}`);
}
public get propA(): string {
return `${this.prop}+A`;
}
// setter 方法不允许进行返回值的类型标注
public set propA(value: string) {
this.propA = `${value}+A`;
}
// 静态成员,类的内部静态成员无法通过 this 来访问
static staticHandler() {}
public instanceHandler() {}
}
- public:此类成员在类、类的实例、子类中都能被访问。
- private:此类成员仅能在类的内部被访问。
- protected:此类成员仅能在类与子类中被访问。
- static:静态成员直接被挂载在函数体上,而实例成员挂载在原型上。静态成员不会被实例继承,它始 终只属于当前定义的这个类(以及其子类)。而原型对象上的实例成员则会被继承。
继承、实现、抽象类
class Base {
print() {}
}
class Derived extends Base {
print() {
super.print();
// ...
}
}
抽象类使用 abstract 关键字声明:
abstract class AbsFoo {
abstract absProp: string;
abstract get absGetter(): string;
abstract absMethod(name: string): string;
}
注意,抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现 (implements)一个抽象类:
class Foo implements AbsFoo {
absProp: string = 'linbudu';
get absGetter() {
return 'linbudu';
}
absMethod(name: string) {
return name;
}
}
此时,我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象 成员。
对于抽象类,它的本质就是描述类的结构。
内置类型:any 、unknown 与 never
- any 放弃了所有的类型检查,而 unknown 并没有。
- any 与 unknown 是比原始类型、对象类型等更广泛的类型,也就是说它们更上层一些。
- never 类型不携带任何的类型信息,因此会在联合类型中被直接移除。
在编程语言的类型系统中,never 类型被称为 Bottom Type,是整个类型系统层级中最底层的类型。和 null、undefined 一样,它是所有类型的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型变量。
类型断言:警告编译器不准报错
let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo();
双重断言
const str: string = 'linbudu';
(str as unknown as { handler: () => {} }).handler();
// 使用尖括号断言
(<{ handler: () => {} }>(<unknown>str)).handler();
非空断言
declare const foo: {
func?: () => {
prop?: number | null;
};
};
foo.func().prop.toFixed(); // 对象可能为 "null" 或“未定义”
foo.func!().prop!.toFixed();
交叉类型
interface NameStruct {
name: string;
}
interface AgeStruct {
age: number;
}
type ProfileStruct = NameStruct & AgeStruct;
const profile: ProfileStruct = {
name: 'linbudu',
age: 18
};
索引类型
索引签名类型
索引签名类型主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构:
interface AllStringTypes {
[key: string]: string;
}
type AllStringTypes = {
[key: string]: string;
};
索引类型查询
interface Foo {
linbudu: 1;
599: 2;
}
type FooKeys = keyof Foo; // "linbudu" | 599
// 在 VS Code 中悬浮鼠标只能看到 'keyof Foo'
// 看不到其中的实际值,你可以这么做:
type FooKeys = keyof Foo & {}; // "linbudu" | 599
索引类型访问
interface NumberRecord {
[key: string]: number;
}
type PropType = NumberRecord[string]; // number
映射类型
映射类型的主要作用即是基于键名映射到键值类型。
type Stringify<T> = {
[K in keyof T]: string;
};
类型查询操作符 typeof
const str = 'linbudu';
const obj = { name: 'linbudu' };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
return input.length > 10;
};
type Str = typeof str; // "linbudu"
type Obj = typeof obj; // { name: string; }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean
在工具类型中使用 typeof:
const func = (input: string) => {
return input.length > 10;
};
const func2: typeof func = (name: string) => {
return name === 'linbudu';
};
ReturnType 这个工具类型会返回一个函数类型中返回值位置的类型:
const func = (input: string) => {
return input.length > 10;
};
type FuncReturnType = ReturnType<typeof func>;
类型守卫
function isString(input: unknown): boolean {
return typeof input === 'string';
}
function foo(input: string | number) {
if (isString(input)) {
// 类型“string | number”上不存在属性“replace”。
input.replace('linbudu', 'linbudu599');
}
if (typeof input === 'number') {
}
// ...
}
类型控制流分析, isString 这个函数在另外一个地方,内部的判断逻辑并不在函数 foo 中。这里的类型控制流 分析做不到跨函数上下文来进行类型的信息收集。TypeScript 引入了 is 关键字来显式地提供类型信息:
function isString(input: unknown): input is string {
return typeof input === 'string';
}
function foo(input: string | number) {
if (isString(input)) {
// 正确了
input.replace('linbudu', 'linbudu599');
}
if (typeof input === 'number') {
}
// ...
}
基于 in 与 instanceof 的类型保护
in
interface Foo {
foo: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
bar: string;
barOnly: boolean;
shared: number;
}
function handle(input: Foo | Bar) {
if ('foo' in input) {
input.fooOnly;
} else {
input.barOnly;
}
}
为什么 shared 不能用来区分?Foo 与 Bar 都满足 'shared' in input 这个条件。因此在 if 分支中, Foo 与 Bar 都会被保留,那在 else 分支中就只剩下 never 类型。
function handle1(input: Foo | Bar) {
if ('shared' in input) {
// 类型“Foo | Bar”上不存在属性“fooOnly”。类型“Bar”上不存在属性“fooOnly”。
input.fooOnly;
} else {
// 类型“never”上不存在属性“barOnly”。
input.barOnly;
}
}
instanceof
interface Foo {
kind: 'foo';
diffType: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
kind: 'bar';
diffType: number;
barOnly: boolean;
shared: number;
}
function handle1(input: Foo | Bar) {
if (input.kind === 'foo') {
input.fooOnly;
} else {
input.barOnly;
}
}
function handle2(input: Foo | Bar) {
// 报错,并没有起到区分的作用,在两个代码块中都是 Foo | Bar
if (typeof input.diffType === 'string') {
input.fooOnly;
} else {
input.barOnly;
}
}
除此之外,JavaScript 中还存在一个功能类似于 typeof 与 in 的操作符:instanceof,它判断的是原型级别的 关系,如 foo instanceof Base 会沿着 foo 的原型链查找 Base.prototype 是否存在其上。
class FooBase {}
class BarBase {}
class Foo extends FooBase {
fooOnly() {}
}
class Bar extends BarBase {
barOnly() {}
}
function handle(input: Foo | Bar) {
if (input instanceof FooBase) {
input.fooOnly();
} else {
input.barOnly();
}
}
类型断言守卫
let name1: any = 'linbudu';
function assertIsNumber(val: any): asserts val is number {
if (typeof val !== 'number') {
throw new Error('Not a number!');
}
}
assertIsNumber(name1);
// number 类型!
name1.toFixed();
类型别名中的泛型
type Factory<T> = T | number | string;
type Stringify<T> = {
[K in keyof T]: string;
};
type Clone<T> = {
[K in keyof T]: T[K];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
默认值
type Factory<T = boolean> = T | number | string;
使用 extends 关键字来约束传入的泛型参数必须符合要求
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"
type Res3 = ResStatus<'10000'>; // 类型“string”不满足约束“number”。
多泛型关联
type Conditional<Type, Condition, TruthyResult, FalsyResult> = Type extends Condition
? TruthyResult
: FalsyResult;
// "passed!"
type Result1 = Conditional<'linbudu', string, 'passed!', 'rejected!'>;
// "rejected!"
type Result2 = Conditional<'linbudu', boolean, 'passed!', 'rejected!'>;
对象类型中的泛型
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
function fetchUserProfile(userId: number): Promise<IRes<IUserProfileRes>> {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => data as IRes<IUserProfileRes>); // 将数据类型断言为泛型类型 T
}
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
interface IPaginationRes<TItem = unknown> {
data: TItem[];
page: number;
totalCount: number;
hasNextPage: boolean;
}
function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>> {
return fetch(`https://api.example.com/users`)
.then(response => response.json())
.then(data => data as IRes<IPaginationRes<IUserProfileRes>>); // 将数据类型断言为泛型类型 T
}
函数中的泛型
function swap<T, U>([start, end]: [T, U]): [U, T] {
return [end, start];
}
const swapped1 = swap(['linbudu', 599]);
函数的泛型参数也会被内部的逻辑消费,如:
function handle<T>(payload: T): Promise<[T]> {
return new Promise<[T]>((res, rej) => {
res([payload]);
});
}
对于箭头函数的泛型,其书写方式是这样的:
const handle = <T>(input: T): T => {};
// 或者
const handle = <T extends any>(input: T): T => {};
Class 中的泛型
class Queue<TElementType> {
private _list: TElementType[];
constructor(initial: TElementType[]) {
this._list = initial;
}
// 入队一个队列泛型子类型的元素
enqueue<TType extends TElementType>(ele: TType): TElementType[] {
this._list.push(ele);
return this._list;
}
// 入队一个任意类型元素(无需为队列泛型子类型)
enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
return [...this._list, element];
}
// 出队
dequeue(): TElementType[] {
this._list.shift();
return this._list;
}
}
内置方法中的泛型
interface PromiseConstructor {
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
}
declare var Promise: PromiseConstructor;
结构化类型系统
类型兼容性判断的幕后
class Cat {
meow() {}
eat() {}
}
class Dog {
eat() {}
}
function feedCat(cat: Cat) {}
// 报错!
feedCat(new Dog());
TypeScript 比较两个类型并非通过类型的名称,而是比较这两个类型上实际拥有的属性与方法。也就是说,这里 实际上是比较 Cat 类型上的属性是否都存在于 Dog 类型上。
class Cat {
eat() {}
}
class Dog {
bark() {}
eat() {}
}
function feedCat(cat: Cat) {}
feedCat(new Dog());
这个时候为什么却没有类型报错了?这是因为,结构化类型系统认为 Dog 类型完全实现了 Cat 类型。至于额外的 方法 bark,可以认为是 Dog 类型继承 Cat 类型后添加的新方法,即此时 Dog 类可以被认为是 Cat 类的子类。
更进一步,在比较对象类型的属性时,同样会采用结构化类型系统进行判断。而对结构中的函数类型(即方法)进 行比较时,同样存在类型的兼容性比较:
class Cat {
eat(): boolean {
return true;
}
}
class Dog {
eat(): number {
return 599;
}
}
function feedCat(cat: Cat) {}
// 报错!
feedCat(new Dog());
标称类型系统
标称类型系统(Nominal Typing System)要求,两个可兼容的类型,其名称必须是完全一致的,比如以下代码:
type USD = number;
type CNY = number;
const CNYCount: CNY = 200;
const USDCount: USD = 200;
function addCNY(source: CNY, input: CNY) {
return source + input;
}
addCNY(CNYCount, USDCount);
在结构化类型系统中,USD 与 CNY 被认为是两个完全一致的类型。在标称类型系统中,CNY 与 USD 被认为是两个 完全不同的类型,因此能够避免这一情况发生。
在 TypeScript 中模拟标称类型系统
通过交叉类型的方式来实现信息的附加:
export declare class TagProtector<T extends string> {
protected __tag__: T;
}
export type Nominal<T, U extends string> = T & TagProtector<U>;
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);
// 报错了!
addCNY(CNYCount, USDCount);
这一实现方式本质上只在类型层面做了数据的处理,在运行时无法进行进一步的限制。我们还可以从逻辑层面入手 进一步确保安全性:
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);
// 报错了!
addCNY(CNYCount, USDCount);
类型系统层级
从原始类型开始
type Result1 = 'linbudu' extends string ? 1 : 2; // 1
type Result2 = 1 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: 'linbudu' } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1
字面量类型 < 对应的原始类型。
联合类型
type Result7 = 1 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result8 = 'lin' extends 'lin' | 'bu' | 'du' ? 1 : 2; // 1
type Result9 = true extends true | false ? 1 : 2; // 1
字面量类型 < 包含此字面量类型的联合类型,原始类型 < 包含此原始类型的联合类型
装箱类型
type Result14 = string extends String ? 1 : 2; // 1
type Result15 = String extends {} ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends Object ? 1 : 2; // 1
在结构化类型系统的比较下,String 会被认为是 {} 的子类型。
由于结构化类型系统这一特性的存在,矛盾的结论:
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends {} ? 1 : 2; // 1
type Result17 = object extends Object ? 1 : 2; // 1
type Result20 = Object extends object ? 1 : 2; // 1
type Result19 = Object extends {} ? 1 : 2; // 1
type Result21 = {} extends Object ? 1 : 2; // 1
16-18 和 19-21 这两对,为什么无论如何判断都成立?难道说明 {}
和 object 类型相等,也和 Object
类
型一致?
当然不,这里的 {} extends
和 extends {}
实际上是两种完全不同的比较方式。{} extends object
和
{} extends Object
意味着, {}
是 object 和 Object 的字面量类型,是从类型信息的层面出发的,
即字面量类型在基础类型之上提供了更详细的类型信息。object extends {}
和 Object extends {}
则
是从结构化类型系统的比较出发的,即 {}
作为一个一无所有的空对象,几乎可以被视作是所有类型的基类
,万物的起源。如果混淆了这两种类型比较的方式,就可能会得到 string extends object
这样的错误结论。
而 object extends Object
和 Object extends object
这两者的情况就要特殊一些,它们是因为“系统设定”
的问题,Object 包含了所有除 Top Type 以外的类型(基础类型、函数类型等),object 包含了所有非原始类型
的类型,即数组、对象与函数类型,这就导致了你中有我、我中有你的神奇现象。
在这里,我们暂时只关注从类型信息层面出发的部分,即结论为:原始类型 < 原始类型对应的装箱类型 < Object 类型。
Top Type
Object 类型自然会是 any 与 unknown 类型的子类型。
type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1
但如果我们把条件类型的两端对调一下呢?
type Result24 = any extends Object ? 1 : 2; // 1 | 2
type Result25 = unknown extends Object ? 1 : 2; // 2
你会发现,any 竟然调过来,值竟然变成了 1 | 2
?我们再多试几个看看:
type Result26 = any extends 'linbudu' ? 1 : 2; // 1 | 2
type Result27 = any extends string ? 1 : 2; // 1 | 2
type Result28 = any extends {} ? 1 : 2; // 1 | 2
type Result29 = any extends never ? 1 : 2; // 1 | 2
是不是感觉匪夷所思?
实际上,还是因为“系统设定”的原因。any 代表了任何可能的类型,当我们使用 any extends
时,它包含了
“让条件成立的一部分”,以及“让条件不成立的一部分”。而从实现上说,在 TypeScript 内部代码的条件
类型处理中,如果接受判断的是 any,那么会直接返回条件类型结果组成的联合类型。
never
never 类型代表了“虚无”的类型,一个根本不存在的类型。对于这样的类型,它会是任何类型的子类型,当然也包 括字面量类型:
type Result33 = never extends 'linbudu' ? 1 : 2; // 1
但你可能又想到了一些特别的部分,比如 null、undefined、void。
type Result34 = undefined extends 'linbudu' ? 1 : 2; // 2
type Result35 = null extends 'linbudu' ? 1 : 2; // 2
type Result36 = void extends 'linbudu' ? 1 : 2; // 2
上面三种情况当然不应该成立。
在 TypeScript 中,void、undefined、null 都是切实存在、有实际意义的类型,它们和 string、number、object 并没有什么本质区别。
类型层级链
type VerboseTypeChain /* 8 */ = never extends 'linbudu'
? 'linbudu' extends 'linbudu' | 'budulin'
? 'linbudu' | 'budulin' extends string
? string extends {}
? string extends String
? String extends {}
? {} extends object
? object extends {}
? {} extends Object
? Object extends {}
? object extends Object
? Object extends object
? Object extends any
? Object extends unknown
? any extends unknown
? unknown extends any
? 8
: 7
: 6
: 5
: 4
: 3
: 2
: 1
: 0
: -1
: -2
: -3
: -4
: -5
: -6
: -7
: -8;
条件类型基础
function universalAdd<T extends number | bigint | string>(x: T, y: T): LiteralToPrimitive<T> {
return x + (y as any);
}
export type LiteralToPrimitive<T> = T extends number
? number
: T extends bigint
? bigint
: T extends string
? string
: never;
universalAdd('linbudu', '599'); // string
universalAdd(599, 1); // number
universalAdd(10n, 10n); // bigint
infer 关键字
TypeScript 中支持通过 infer 关键字来在条件类型中提取类型的某一部分信息
// 提取首尾两个
type ExtractStartAndEnd<T extends any[]> = T extends [infer Start, ...any[], infer End]
? [Start, End]
: T;
// 调换首尾两个
type SwapStartAndEnd<T extends any[]> = T extends [infer Start, ...infer Left, infer End]
? [End, ...Left, Start]
: T;
// 调换开头两个
type SwapFirstTwo<T extends any[]> = T extends [infer Start1, infer Start2, ...infer Left]
? [Start2, Start1, ...Left]
: T;
工具类型
属性修饰工具类型
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 Record<K extends keyof any, T> = {
[P in K]: T;
};
// 工具类库源码:
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,它接受两个泛型参数,T 即是我们会进行结构处理的原类型(一般是对象类型),而 K 则被约束 为 T 类型的键名联合类型。由于泛型约束是立即填充推导的,即你为第一个泛型参数传入 Foo 类型以后,K 的约 束条件会立刻被填充,因此在你输入 K 时会获得代码提示:
问题
interface 与 type 异同点
- 在对象扩展情况下,interface 使用 extends 关键字,而 type 使用交叉类型(
&
)。 - 同名的 interface 会自动合并,并且在合并时会要求兼容原接口的结构。
- interface 与 type 都可以描述对象类型、函数类型、Class 类型,但 interface 无法像 type 那样表达元组 、一组联合类型等等。
- interface 无法使用映射类型等类型工具,也就意味着在类型编程场景中我们还是应该使用 type 。
对这两个工具的理解:interface 就是描述对象对外暴露的接口,其不应该具有过于复杂的类型逻辑,最多局限 于泛型约束与索引类型这个层面。而 type alias 就是用于将一组类型的重命名,或是对类型进行复杂编程。
类型兼容性比较
TypeScript 使用鸭子类型,也即结构化类型系统进行类型兼容性的比较,即对于两个属性完全一致的类型,就认 为它们属于同一种类型。而对于 A 类型、A + B 类型,认为后者属于前者的子类型。另外 TypeScript 类型中还 存在着一部分特殊的规则,如 object、{} 以及 Top Type 等。
- 结构化类型系统到标称类型系统,你可以表达你不仅了解结构化类型系统,还了解与其可以作为对比的标称 类型系统,包括存在意义与比较方式,以及如何在 TS 中实现标称类型系统。
- 类型层级,类型兼容性的比较本质上其实也就是在类型层级中进行比较,一个类型能够兼容其子类型,就这 么回事,因此,不妨扩展地讲一讲 TS 的类型层级是怎么样的
any、unknown 与 never
any 与 unknown 在 TypeScript 类型层级中属于最顶层的 Top Type,也就意味所有类型都是它俩的子类型。而 never 则相反,作为 Bottom Type 的它是所有类型的子类型。
- 为什么需要 Top Type 与 Bottom Type ? 在实际开发中,我们不可能确保对所有地方的类型都进行精确的 描述,因此就需要 Top Type 来表示一个包含任意类型的类型。而在类型编程中,如果对两个不存在交集的类型 强行进行交集运算,也需要一个类型表示这个不存在的类型。这就是 Top Type 与 Bottom Type 的存在意义。
- 类型层级,Top 与 Bottom 本身就是在描述它们在类型层级中的位置,因此,如果你能给面试官讲一遍从 Bottom 向上到 Top 的类型链,我觉得起码在 TypeScript 这个技能点上你已经基本得到肯定了。
- 条件类型,Top Type 与 Bottom Type 带来的底层规则还不止表现在类型兼容性方面,在条件类型中同样存 在对它们的特殊逻辑,请回想 any 与 never 在条件类型中的表现。
工具类型实现
比较简单的工具类型手写可能包括 Partial(Require)、Pick(Omit)、ReturnType(ParameterType),小册中 均已介绍了相关实现与原理,这里就不再赘述。
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameterType<T extends (...args: any[]) => any> = T extends (...args: infer P) => any
? P
: never;
type MyExclude<T, U> = T extends U ? never : T;
优秀回答
- 我不仅能写出这些基础实现,还能写出其在实际应用场景中的增强版,比如 DeepPartial 与 MarkAsPartial,PickByType 与 PickByStrictType 等等。
- 我不仅了解这些工具类型的实现,还了解它们可以被归纳为访问性修饰工具类型、结构处理工具类型、集合工具 类型与模式匹配工具类型等等,同时对它们实现过程中使用到的类型工具也有较为深入的了解。