跳到主要内容

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”。

为了更好地区分 Objectobject 以及{}这三个具有迷惑性的类型,我们再做下总结:

  • 在任何时候都不要使用 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 ObjectObject 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 等等。
  • 我不仅了解这些工具类型的实现,还了解它们可以被归纳为访问性修饰工具类型、结构处理工具类型、集合工具 类型与模式匹配工具类型等等,同时对它们实现过程中使用到的类型工具也有较为深入的了解。