TS使用技巧

使用类型声明而非类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Circle = { kind: "circle"; radius: number };
type Rect = { kind: "rect"; width: number; height: number };
type Shape = Circle | Rect;

function isCircle(shape: Shape) {
return shape.kind === "circle";
}

function isRect(shape: Shape) {
return shape.kind === "rect";
}

const myShapes: Shape[] = getShapes();

// 错误:因为ts无法正确识别 filter 后的类型
const circles: Circle[] = myShapes.filter(isCircle);

// 可能会使用这种类型断言的方式
// const circles = myShapes.filter(isCircle) as Circle[];

更优雅的解决方案是将isCircleisRect改为返回类型声明
这样TS在调用filter之后可以缩小类型范围:

1
2
3
4
5
6
7
8
9
10
11
12
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}

function isRect(shape: Shape): shape is Rect {
return shape.kind === "rect";
}

const myShapes: Shape[] = getShapes();

// 现在可以用 filter 获取到正确的类型
const circles = myShapes.filter(isCircle);

推荐使用type替代interface


在 TypeScript 中,一个没有返回值(即没有调用 return 语句)的函数,其返回类型应当被标记为 void 而不是 undefined,即使它实际的值是 undefined。

1
2
3
4
5
6
7
// 没有调用 return 语句
function foo(): void {}

// 进行了返回操作,但没有返回实际的值。就使用 undefined
function bar(): undefined {
return;
}

要想实现与入参关联的返回值类型,我们可以使用 TypeScript 提供的函数重载签名

1
2
3
4
5
6
7
8
9
10
11
12
13
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

unknownany 的一个主要差异体现在赋值给别的变量时,any 就像是 “我身化万千无处不在” ,所有类型都把它当自己人。而 unknown 就像是 “我虽然身化万千,但我坚信我在未来的某一刻会得到一个确定的类型” ,


in进行类型推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Foo {
foo: string;
fooOnly: boolean;
shared: number;
name: string;
}

interface Bar {
bar: string;
barOnly: boolean;
shared: number;
age: number;
}

function handle(input: Foo | Bar) {
if ("bar" in input) {
// 推导为Bar类型,所以报错:类型“Bar”上不存在属性“fooOnly”
input.fooOnly;
} else {
input.barOnly;
}
}

常用工具类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

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

类型导入

1
2
import { Foo } from "./foo";
import type { FooType } from "./foo";

TS 工具

ts-nodets-node-dev:我们在环境搭建一节中已经介绍过,用于直接执行 .ts 文件。其中ts-node-dev基于ts-nodenode-dev(类似于 nodemon)封装,能够实现监听文件改动并重新执行文件的能力。

tsc-watch:它类似于 ts-node-dev,主要功能也是监听文件变化然后重新执行,但tsc-watch的编译过程更明显,也需要自己执行编译后的文件。你也可以通过 onSuccess 与 onFailure 参数,来在编译过程成功与失效时执行不同的逻辑。
esno: antfu 的作品。核心能力同样是执行 .ts 文件,但底层是 ESBuild 而非 tsc,因此速度上会明显更快。
ts-error-translator,将 TS 报错翻译成更接地气的版本,并且会根据代码所在的上下文来详细说明报错原因,目前只有英文版本,中文版本感觉遥遥无期,因为 TS 的报错实在太多了……
typescript-json-schema:从 TypeScript 代码生成 JSON Schema,如以下代码:

1
2
3
4
5
6
7
8
9
10
// From
export interface Shape {
/**
* The size of the shape.
*
* @minimum 0
* @TJS-type integer
*/
size: number;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Result
{
"$ref": "#/definitions/Shape",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Shape": {
"properties": {
"size": {
"description": "The size of the shape.",
"minimum": 0,
"type": "integer"
}
},
"type": "object"
}
}
}

json-schema-to-typescript,和上面那位反过来,从 JSON Schema 生成 TypeScript 代码:

type-fest,不用多介绍了,目前 star 最多下载量最高的工具类型库,Sindre Sorhus 的作品,同时也是个人认为最接地气的一个工具类型库。
utility-types,包含的类型较少,但这个库是我类型编程的启蒙课,我们此前对 FunctionKeys、RequiredKeys 等工具类型的实现就来自于这个库。
ts-essentials
type-zoo
ts-toolbelt,目前包含工具类型数量最多的一位,基本上能满足你的所有需要。
tsd,用于进行类型层面的单元测试,即验证工具类型计算结果是否是符合预期的类型,也是 Sindre Sorhus 的作品,同时 type-fest 中工具类型的单元测试就是基于它。
conditional-type-checks,类似于 tsd,也是用于对类型进行单元测试。
class-validator,TypeStack 的作品,基于装饰器来进行校验,我们会在后面的装饰器一节了解如何基于装饰器进行校验。