Typescript学习笔记


typescript 类型系统

TypeScript 中,每个类型就是一个集合

基本类型

字面量类型/单元类型

typescript 允许模拟定义具体值类型

1
2
3
4
5
// 限制str的值,使其不允许被修改(模拟const)
let str: "wdnmd" = "wdnmd";
// 限制只能是几个值之一
let str1: "red" | "blue" | "green" = "red";
let str2: "color" | true | 1;

一般情况下, typescript 默认认为对象的属性是可以改变的,但在如下情况会出现问题

1
2
3
4
function handle(method: "GET" | "POST") {}
const obj = { method: "GET" };
// 报错:类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。
handle(obj.method);

因为默认推断了 obj.methodstring 类型,然而 string 类型是有可能不属于 'GET' | 'POST' 类型的,因此报错
解决方法如下

1
2
3
4
// 将obj内属性设置为只读
const obj = { method: "GET" } as const;
// 或另一种断言方式
const obj = <const>{ method: "GET" };

bigint

指非常大的整数,使用时需将 target 设置为 es2020

1
const bigintNum: bigint = 100n;

never

never 指为不存在的类型,永远不会被观察到的值,它是所有类型的子类型,用集合来解释即:never 是空集

例如下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Circle = {
kind: "circle";
};

type Square = {
kind: "square";
};

type Sharp = Circle | Square;

function sharpHandle(sharp: Sharp) {
switch (sharp.kind) {
case "circle":
// sharp: Circle;
return sharp;
case "square":
// sharp: Square;
return sharp;
default:
// sharp: never;
return sharp;
}
}

当一个函数抛出一个异常、中止程序执行 或 死循环,返回值的类型就是 never ,也用于联合类型没有匹配到任何东西的情况下的值类型

void

在 ts 中,一个不返回任何值的函数将自动推断返回值为 void 类型
而在 js 中,这种函数的返回值默认为 undefined

ts 中的 voidundefined 是不一样的

object

该类型指的是任何不是基元的值,其并不同于 {} ,也不同于 Object

基元: string number bigint boolean symbol null undefined

unknown

该类型代表任何值,和 any 类似,但更安全,因为对未知的 unknown 值做任何事都是不合法的

仅允许将 unknown 类型赋值给 anyunknown 类型

当使用 typeof 关键字对其进行类型提取时,与 any 的表现也不一样:

1
2
3
4
// type Any = string | number | symbol
type Any = keyof any
// type Unknown = never
type Unknown = keyof unknown

unknown 与任何类型交叉,均返回对方类型
与除 any 外的任何类型联合,均返回 unknown 类型

*注:因为 any 与任何类型联合或与除 never 以外的类型交叉均返回 any 类型

Function

注意大写,该类型描述了诸如 bind call apply 以及其他存在于 js 中所有函数值的属性。

Function 类型的值总是可以被调用,调用的方法均返回 any 。因此,并不推荐使用 Function 去定义函数类型。

元组

js 中并不支持元组,而 ts 支持,格式:[typeA, typeB]

1
const normal: [number, string] = [9, "baka"];

函数

定义方法

类型别名和接口定义函数的方法分别为

1
2
3
4
5
6
/* 接口 */
interface Handle {
(arg: number): boolean;
}
/* 类型别名 */
type Handle = (arg: number) => boolean;

注:在函数类型的形参中,ts 仅限制了形参的最大数量以及每个形参的类型,对形参的名字无限制,允许定义的形参少于类型中形参的个数

1
2
3
4
type Handle = (arg1: number, arg2: booleab) => boolean;
// 少定义了一个参数,不会报错
// 等号右侧不需要明显定义类型其实,ts会自动推断
const handle: Handle = (wdnmd: number): boolean => true;

函数签名

函数允许自带属性,类型定义方式如下

1
2
3
4
5
6
7
8
type Handle = {
desc: string;
(arg: number): boolean;
};

const handle: Handle = (wdnmd) => wdnmd === 1;
// 不定义会报错
handle.desc = "";

调用签名

即为函数的类型,如下表示:此函数有一个 string 类型的参数,无返回值的。且存在一个类型为 string 的属性 desc

1
2
3
4
5
6
7
8
interface IFunc {
(wdnmd: string): boolean;
desc: string;
}

const handle: IFunc = (arg: string) => !!arg;
// 没有下面这一行会报错,因为desc为必须的属性
handle.desc = "函数的属性";

构造签名

即为 class 的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TsClass {
private arg: string;
constructor(arg: string) {
this.arg = arg;
}
}

interface ITsc {
new (wdnmd: string): TsClass
}

// 或使用类型别名
// type ITsc = new (wdnmd: string) => TsClass

function getTsClass(tsc: ITsc) {
return new tsc('wdnmd');
}

若将形参 tsc 的类型修改为 TsClass ,则会出现以下错误

1
2
3
4
function getTsClass(tsc: TsClass) {
// 类型 "TsClass" 没有构造签名
return new tsc("wdnmd");
}

可选参数

不建议在回调函数中定义可选参数

函数重载

提供 {} 表示对函数的实现,这里称其为为 实现函数,反之为 重载函数

实现函数虽然定义了参数可选,但那是为了兼容两个重载函数,调用时仍然只能使用两种重载的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function overload(timeStamp: number): Date;
function overload(d: number, m: number, y: number): Date;

/* 函数实现 */
function overload(dOrTimeStamp: number, m?: number, y?: number): Date {
if (m && y) {
return new Date(y, m, dOrTimeStamp);
} else {
return new Date(dOrTimeStamp);
}
}

overload(1650819030751);
overload(25, 4, 2022);
// 报错:没有需要 2 参数的重载,但存在需要 1 或 3 参数的重载。
overload(25, 4);

调用函数时,是看不到实现函数的参数的,只能看到重载函数的参数

1
2
3
4
function overload(field: string): void;
// 不会报错
function overload() {}
overload("this is a field");

实现函数若存在参数,则必须要兼容所有的重载函数的参数类型,返回值同样需要兼容

1
2
3
4
5
6
7
8
function overload(field: string): string;
function overload(field: number): number;

function overload(field: string | number): string | number {
return "field";
}

overload("this is a field");

可以的话,更推荐用 联合类型的参数 而不是重载参数,因为 重载参数 不能使用两者均有可能的值作为参数。

1
2
3
4
5
6
7
function overload(field: string): void;
function overload(field: number): void;

const field: string | number = Math.random() > 0.5 ? "" : 0;
// 类型“string | number”的参数不能赋给类型“string”的参数。
// 类型“string | number”的参数不能赋给类型“number”的参数。
overload(field);

this 入参

js 中不支持 this 作为函数参数,但 ts 允许这样做。
ts 允许在函数入参列表中第一个位置处,手动写入this并标记其类型。它仅仅是一个形式上的参数,并不会在编译后的代码中出现,仅仅是为了提供给 Typescript 编译器作为检查静态类型使用。

通常我们会在需要纠正 this 类型的场合下使用这个机制,参考如下示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Handle {
public value: string;
constructor(value: string) {
this.value = value
}

onClick(this: Handle, e: Event) {
this.value = e.type;
}
}

interface myElement {
addClickListener(onClick: (this: void, e: Event) => void): void
};

const ele: myElement = {
addClickListener(onClick: (this: void, e: Event) => void) {
onClick({ type: 'click' } as Event)
}
};

// TS2345: Argument of type "(this: Handle, e: Event) => void" is not assignable to parameter of type "(this: void, e: Event) => void".
ele.addClickListener(new Handle("default").onClick);

由于众所周知的 this 指向问题,上面代码中在调用 onClick 时,this 指向 undefined,而在 onClick 函数中期望的 this 则是指向 Handle 类。经过限制后 ts 捕获到了错误并作出了提醒。若不做限制(两处有任意一处未对 this 进行定义),则会出现 Cannot set property 'value' of undefined 报错。

参数展开

ts 可以给 es6 中的函数形参展开式定义类型

1
2
3
4
5
6
// 此处 m 若不做具体类型定义,则为 any[] 类型
function mutiply(n: string, ...m: number[]) {
return m.map((x) => n.padEnd(x, n));
}

mutiply("wdnmd", 6, 7, 8, 9, 10);

同理,也可以展开数组实参传入方法,如下:

1
2
3
const list: string[] = [];
const list2: string[] = ["a", "b"];
list.push(...list2);

参数解构

1
2
3
4
5
6
7
8
9
10
type Parm = {
label: string;
value: number;
};

function handle({ label, value }: Parm) {
return `${label}: ${value}`;
}

handle({ label: "baka", value: 9 });

函数的 void 返回类型

当把函数类型定义为 返回 void 的函数 时,函数内允许返回任意类型,ts 会将其忽略,执行函数得到的结果被 ts 判断为 void 类型

1
2
3
4
5
6
7
8
type VoidFun = () => void;

// 不会报错
const v1: VoidFun = () => true;
// const res: void
const res = v1();
// true
console.log(res);

但若直接显式的给函数定义返回值类型为 void ,那么这个函数必须不能返回任何值

1
2
// 不能将类型“boolean”分配给类型“void”。
const v2 = (): void => true;

对象

定义方法

常见有下面几种定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 匿名
const obj: { label: string; value: number } = {
label: "baka",
value: 9,
};

// 接口
interface INormal {
label: string;
value: number;
}

const obj: INormal = {
label: "baka",
value: 9,
};

// 类型别名
type Normal = {
label: string;
value: number;
};

const obj: Normal = {
label: "baka",
value: 9,
};

属性修饰符

可选属性

1
2
3
4
interface INormal {
label?: string;
value: number;
}

只读属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface INormal {
readonly label: string;
value: number;
}

// ps: 如下这种情况并不会阻止修改 value 内部 title 或 count 的修改
interface INormal {
readonly value: {
title: string;
count: number;
};
}

obj.value.count = 99; // 不会报错

索引签名

格式为 { [key: KeyType]: ValueType }

其中 KeyType 仅能使用 numberstringsymbol模板字符串类型(类型章节有讲)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基本使用
interface INormal {
[key: string]: number;
}

// 携带其他参数,必须和索引签名指定类型兼容
interface INormal {
[key: string]: number;
title: number;
}

// 只读属性
interface INormal {
readonly [key: string]: number;
}

Record 区别,Record 的 key 支持使用 字符串字面量类型与联合类型

类型扩展

接口

使用 extends 关键字对接口进行扩展,在后面继承部分会详细说明

接口和类型别名可以互相继承

1
2
3
4
5
6
7
8
9
// 继承单个
interface c extends a {
sex: string;
}

// 继承多个
interface d extends a, b {
address: string;
}

类型别名

使用交叉类型对类型别名进行扩展,交叉类型会在后面详细说明

接口和类型别名可以互相交叉

1
2
3
4
5
6
7
8
type c = a & {
sex: string;
};

type d = a &
b & {
address: string;
};

向现有类型添加字段

类型别名一经创建无法更改,无法添加字段

1
2
3
4
5
6
7
/* 接口 */
interface a {
name: string;
}
interface a {
age: number;
}

类型推断

三元表达式

ts 会根据三元表达式冒号两侧自动推断类型

1
2
// string: number | string
const string = Math.radom() > 0.5 ? 10 : "string";

函数返回值

变量经过函数内处理后返回,会自动进行类型缩小

1
2
3
4
5
6
function handle() {
let x: number | string | boolean;
x = Math.random() < 0.5 ? 10 : "string";
// x: string | number
return x;
}

类型断言

两种断言方法

1
2
let str = "string" as any;
let str = <any>"string";

typescript 只允许类型断言转换为更具体不太具体的类型,如不允许将 number 类型断言为 string

当不知道类型时,断言为 unknown 语义上比 any 更确切

类型谓词 is

对函数返回值定义类型 fieldName is Type ,当函数返回 true 时,变量会被指定为对应的类型
若不使用 is ,函数会被认为返回值类型是简单的 boolean ,变量依然是联合类型,无法确切定义具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type bird = {
fly: () => void;
};

type fish = {
swim: () => void;
};

/* 使用类型谓词is */
function isBird(pet: bird | fish): pet is bird {
return (<bird>pet).fly !== undefined;
}

function getPet(): bird | fish {
const fish: fish = {
swim: () => {},
};
const bird: bird = {
fly: () => {},
};
return Math.random() < 0.5 ? fish : bird;
}

const pet = getPet();

// typescript会自动推断不同条件下,pet的类型
isBird(pet) ? pet.fly() : pet.swim();

也可以用来过滤数组

1
2
3
4
5
const zoos: Array<fish | bird> = [getPet(), getPet(), getPet()];
// fishs: fish[]
const fishs = zoos.filter(isFish);
// fishs1: fish[]
const fishs1 = zoos.filter((pet): pet is fish => isFish(pet));

关键字

keyof

提取一个类型的所有键值组成一个新的类型

1
2
3
4
5
6
7
type Normal = {
label: string;
value: number;
};

// "label" | "value"
type KeysNormal = keyof Normal;

示例:获取一个类型的所有值的类型

1
2
3
4
5
6
type Normal = {
label: string
value: number
}
// type Values = string | number
type Values = Normal[keyof Normal]

示例:为获取对象属性的方法定义类型

1
2
3
function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[keyof T] {
return obj[key]
}

keyof 用于基本类型时,将会提取该类型的所有可用方法

1
2
3
4
// type Num = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type Num = keyof number
// type Bool = "valueOf"
type Bool = keyof boolean

keyof 也可以用于 class,此时不会获取 class 中的 privateprotected 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
private label: string
public value: number
constructor(label: string, value: number) {
this.label = label
this.value = value
}
getValue() {
return this.value
}
}
// type Normal = "value" | "getValue"
type Normal = keyof Base

keyof 可以获取枚举类型,但直接使用会获得枚举属性的方法,想获取她的键值,需要结合 typeof 使用

1
2
3
4
5
6
7
8
enum HttpMethod {
Get,
Post
}
// type FakeMethod = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type FakeMethod = keyof HttpMethod
// type Method = "Get" | "Post"
type Method = keyof typeof HttpMethod

对索引签名使用

1
2
3
4
5
6
7
interface normal {
[fieldName: string]: number;
}

// number | string
// 这里允许 number 是因为索引签名特点,当索引签名类型为 string 时,也允许使用 number
type KeysNormal = keyof normal;

对类使用

1
2
3
4
5
6
7
8
9
10
11
12
13
class Normal {
value: string;
constructor(value: string) {
this.value = value;
}

nFun() {
return "normal";
}
}

// "value" | "nFun"
type KeysNormal = keyof Normal;

typeof

提取一个变量的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 解析函数
function normal(arg: string): boolean {
return !!arg;
}
// type Normal = (arg: string) => boolean
type Normal = typeof normal;

// 解析对象
const obj = {
label: "baka",
value: 9,
};
// type Obj = { label: string; value: number; }
type Obj = typeof obj;

提取 any 和 unknown 类型

anyunknown 进行类型提取时则有比较特殊的结果

1
2
3
4
// type Any = string | number | symbol
type Any = keyof any
// type Unknown = never
type Unknown = keyof unknown

infer

infer 关键字用于声明类型变量,以存储在模式匹配过程中所捕获的类型

下面这个示例就捕获到了 number[] 中的 number 类型

1
2
3
type Normal<T> = T extends (infer U)[] ? U : T;
// type T0 = number
type T0 = Normal<number[]>;

声明两个类型变量,捕获对象中的类型

1
2
3
4
5
6
7
8
type Normal<T> = T extends { label: infer U; value: infer R } ? [U, R] : T;

type Base = {
label: string;
value: number;
};
// type T0 = [string, number]
type T0 = Normal<Base>;

infer 使用注意:

  • 仅可以在条件类型extends 子句中使用
  • infer 声明的类型变量 仅可以在条件类型的 true 分支使用

协变与逆变

改编上面示例,两个 infer 均指向同一个变量,结果为联合类型 (协变位置)

1
2
3
4
5
6
7
8
type Normal<T> = T extends { label: infer U; value: infer U } ? U : T;

type Base = {
label: string;
value: number;
};
// type T0 = string | number
type T0 = Normal<Base>;

再做改编,将 infer 指向函数形参,结果为交叉类型 (逆变位置)

1
2
3
4
5
6
7
8
9
10
11
12
13
type Base = {
label: (arg: string) => void;
value: (arg: number) => void;
};

type Normal<T> = T extends {
label: (arg: infer U) => void;
value: (arg: infer U) => void;
}
? U
: T;
// type T0 = never (这里其实是 string & number = never, 变成了交叉类型)
type T0 = Normal<Base>;

函数参数逆变的,而对象属性协变的。
逆变位置的同一类型变量中的多个候选会被推断成交叉类型

基本结构

1
2
3
4
5
6
7
8
class Normal {
label: string;
value: number;
constructor() {
this.label = "";
this.value = 0;
}
}

构造函数

构造函数在执行 new 创建对象时执行,内部允许初始化类的成员属性

1
2
3
4
5
6
7
8
9
class Normal {
label: string;
constructor(label: string) {
this.label = label;
}
}

// "baka"
console.log(new Normal("baka").label);

有两点要注意:

  • 构造函数不能有类型参数
  • 构造函数不能有返回类型注释

允许对构造函数使用参数重载

1
2
3
4
5
6
7
8
class Normal {
label: string;
constructor(name: string, age: number);
constructor(sex: boolean);
constructor(ns: string | boolean, age?: number) {
// ...
}
}

参数属性

即在构造函数 constructor 参数上定义类成员,由 readonly、public、protected、private 等修饰

1
2
3
4
5
6
7
8
9
10
class Normal {
constructor(public label: string) {}

echo() {
console.log(this.label);
}
}

// baka
new Normal("baka").echo();

Getters/Setters

为两组方法,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Normal {
label: string = "";
get title() {
return this.label;
}
// (parameter) title: string
set title(title) {
this.label = title;
}
}

const normal = new Normal();
// 自动执行 get 内的逻辑
const title = normal.title;
// 自动执行 set 内的逻辑
normal.title = "baka";

存在以下几点特性:

  • 如果存在 getter 但不存在 setter,该属性自动变为只读属性
  • 如果没有指定 setter 参数的类型,会自动从 getter 的返回值类型推断出来
  • getter 和 setter 必须设置相同的成员可见性

setter 的参数类型

Typescript 4.3 后,允许 setter 的参数类型为 getter 返回值的子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Normal {
label: string = "";
get title() {
return this.label;
}
set title(value: string | number) {
if (typeof value === "string") {
this.label = value;
}
}
}

const normal = new Normal();
normal.title = 114514;

索引签名

一旦限制索引签名,类中的属性和方法都将受到约束

1
2
3
4
5
6
7
8
9
class Normal {
[fieldName: string]: boolean | ((...args: string[]) => string);

label: boolean = true;

handle(arg: string) {
return arg;
}
}

implements

实现一个接口或类型别名,当一个类指定 implements 后,必须实现其中的所有必须属性。格式如下:

1
2
3
4
5
6
7
interface Normal {
label: string;
}

class NormalClass implements Normal {
label = "baka";
}

一个类可以实现多个 implements

1
2
3
4
5
6
7
8
9
10
11
12
interface Normal {
label: string;
}

interface Hard {
value: number;
}

class NormalClass implements Normal, Hard {
label = "baka";
value = 9;
}

implement 仅对 class 的成员进行限制,并不会给改变类的类型

1
2
3
4
5
6
7
8
9
10
11
interface Normal {
label: string;
value?: number;
}

class NormalClass implements Normal {
label = "baka";
}

// Property 'value' does not exist on type 'NormalClass'.
new NormalClass().value;

extends

(派生类)继承另一个类(基类),获取其所有private 修饰的属性

当一个类继承另一个类时,若该类存在 constructor 构造函数,则必须指定 super 调用。

若派生类未显示写明构造函数,则默认使用 基类 的构造函数。

super: 调用父类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
label: string = "baka";
}

class Normal extends Base {
// 派生类的构造函数必须包含 "super" 调用。
constructor() {
super();
}
}

// "baka"
console.log(new Normal().label);

派生类为基类的子类型,允许被赋值

1
const base: Base = new Normal();

重写基类方法

派生类允许使用和基类同名的方法,来对其进行重写

但必须要与基类方法兼容,如参数以及返回值类型,类似函数重载(属性也是如此)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
echo(): void {
console.log("baka");
}
}

class Normal extends Base {
echo(message?: string): void {
if (!message) {
// 调用父类方法
super.echo();
} else {
console.log(message);
}
}
}

初始化顺序

当派生类使用 new 关键字创建对象时,内部按照以下顺序执行

  • 字段初始化 (基类)
  • 构造函数运行 (基类)
  • 字段初始化 (派生类)
  • 构造函数运行 (派生类)

es5 继承内置对象问题

typescript.json 中的 target 设置为 es5 时,使用 extends 继承内置对象时会出现一些问题

1
2
3
4
5
6
7
8
9
10
class BakaError extends Error {
constructor(msg: string) {
super(msg);
}
echo() {
return `代码出错了笨蛋!: ${this.message}`;
}
}

const bakaError = new BakaError("abaaba");

编译未报错,运行如下代码,发现运行报错

1
2
3
4
// TypeError: bakaError.echo is not a function
console.log(bakaError.echo());
// false
console.log(bakaError instanceof BakaError);

原因:由于 es5 并没有 class 这种玩意儿,需要手动设置一下原型对象

对 constructor 进行如下修改

1
2
3
4
5
6
7
class BakaError extends Error {
constructor(msg: string) {
super(msg);
// 手动设置原型对象
(<any>Object).setPrototypeOf(this, BakaError.prototype);
}
}

再次执行,正常运行

1
2
3
4
// 代码出错了笨蛋!: abaaba
console.log(bakaError.echo());
// true
console.log(bakaError instanceof BakaError);

类中的 this

由于 js 遗留问题,类方法内的 this 可能会存在乱指向的问题, 比如下面示例

1
2
3
4
5
6
7
8
9
10
11
12
class Normal {
label = "baka";
getLabel() {
return this.label;
}
}
const obj = {
label: "baka is you",
getLabel: new Normal().getLabel,
};
// baka is you
console.log(obj.getLabel());

改编为 箭头函数 即可解决这种情况

1
2
3
4
5
6
7
8
9
10
11
12
class Normal {
label = "baka";
getLabel = () => {
return this.label;
};
}
const obj = {
label: "baka is you",
getLabel: new Normal().getLabel,
};
// baka
console.log(obj.getLabel());

注:改编为箭头函数后,派生类无法再使用 super 调用该方法。 typescript 并不会报错,但在运行时将会抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
getLabel = () => {
console.log("baka");
};
}

class Normal extends Base {
constructor() {
super();
// TypeError: (intermediate value).getLabel is not a function
super.getLabel();
}
}

new Normal();

修饰符

readonly

被修饰的属性不允许在构造函数以外的地方进行修改

1
2
3
4
5
6
7
8
9
10
class Normal {
readonly label: string;
constructor() {
this.label = "";
}
test() {
// 无法分配到 "label" ,因为它是只读属性。
this.label = "?";
}
}

public

表示类成员在任何地方都可以访问,是默认值,不指定时默认为 public

protected

protected 修饰的成员只对当前类子类可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
protected label: string = "baka";
}

class Normal extends Base {
getLabel() {
// 允许访问
console.log(this.label);
}
}

// 报错:属性“label”受保护,只能在类“Base”及其子类中访问。
new Normal().label;

派生类可以暴露基类中 protocted 所修饰的成员。

1
2
3
4
5
6
7
8
9
10
class Base {
protected label: string = "baka";
}

class Normal extends Base {
// 暴露基类属性 label
public label: string = "baka is you";
}
// 允许访问
new Normal().label;

private

private 修饰的成员只对当前类可见。

static

static 修饰的成员为静态成员,访问其不需要实例化,直接用类名就可以访问

1
2
3
4
5
class Base {
static label: string = "baka";
}
// 允许访问
Base.label;

静态成员允许被继承,且可以被派生类直接用类名访问

1
2
3
4
5
6
7
class Base {
static label: string = "baka";
}

class Normal extends Base {}
// 允许访问
Normal.label;

es2015 以上支持使用 专用标识符 # 修饰静态成员,如下例的 #label
修饰后的成员变为私有属性,仅允许类内部通过类名调用。

static 支持修饰一个代码块,称为 static 区块,区块内同样可以访问 # 静态成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Normal {
static #label = "baka";

get label() {
return Normal.#label;
}

// static 区块
static {
this.#label = "baka is you";
}
}
// baka is you
console.log(new Normal().label);
// 报错:属性 "#label" 在类 "Normal" 外部不可访问,因为它具有专用标识符。
Normal.#label;
// 报错:属性 “#label” 在类型 “Normal” 上不存在。
new Normal().#label;

在泛型类中,static 修饰的成员是不能使用 类型参数

1
2
3
4
class Normal<T> {
// 报错:静态成员不能引用类类型参数。
static label: T;
}

类表达式

类允许像方法一样用表达式声明一个匿名类并赋值,同样允许使用泛型

1
2
3
4
5
const Normal = class<T> {
constructor(public label: T) {}
};
// "baka"
console.log(new Normal<string>("baka").label);

this 类型

在类中可以使用 this 类型来指向当前 this 所指向的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
label: string = "baka";
echo(other: this) {
return this.label;
}
}

class Normal extends Base {
value: number = 9;
}

// 报错:类型“Base”的参数不能赋给类型“Normal”的参数。
new Normal().echo(new Base());

this is Type

类和接口的方法的返回值的位置使用,缩小调用该方法的目标的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Normal<T> {
public label?: T;

hasLabel(): this is { label: T } {
return !!this.label;
}
}

const normal = new Normal<string>();
normal.label = "baka";
if (normal.hasLabel()) {
// const normal: Normal<string> & { label: string }
normal.label;
}

抽象类与抽象成员

使用 abstract 修饰后即变为抽象类,抽象类不能被实例化,只能被作为基类继承。

使用 abstract 修饰的成员为抽象成员,abstract 关键字于 public readonly 等之后使用,仅允许出现在抽象类中,不可以实现,子类继承后必须实现该成员。

1
2
3
4
5
6
7
8
9
10
11
abstract class Base {
// 不允许实现
public abstract readonly label: string;
// 非抽象成员,可以实现
public value: number = 9;
}

class Normal extends Base {
// 必须要实现 Base 中的抽象成员 label
public label: string = "baka";
}

类之间的细节补充

当两个类的成员类型完全相同时,是可以互相替代的

1
2
3
4
5
6
7
8
9
10
11
class Normal {
label: string = "baka";
value: number = 9;
}

class Hard {
label: string = "baka is you";
value: number = 6;
}
// 不会报错
const normal: Normal = new Hard();

当两个类的成员存在兼容关系但未显式继承时,也是存在隐式继承的

1
2
3
4
5
6
7
8
9
10
class Normal {
label: string = "baka";
}

class Hard {
label: string = "baka is you";
value: number = 6;
}
// 不会报错
const normal: Normal = new Hard();

针对空类时,任何类型都可以是它的实例对象

1
2
3
4
5
6
class Empty {}
// 均不会报错
const a: Empty = "baka";
const b: Empty = 9;
const c: Empty = {};
const d: Empty = () => {};

泛型

定义泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 泛型对象
type Normal<T> = {
value: T;
};

interface Normal<T> {
value: T;
}

// 泛型函数
type Normal<T> = (arg: T) => T;

interface Normal<T> {
(arg: T): T;
}

// 对象中的泛型函数
interface NormalFun {
<T>(ary: T): T;
}

自动推断

泛型是具备自动推断的,下面的示例自动将 T 推断为了 number

1
2
3
4
5
6
7
function genericsFunc<T>(tList: T[]): T | undefined {
return tList[0];
}
// num: number
const num = genericsFunc([1, 2, 3]);
// 手动注释写法
// const num = genericsFunc<number>([1, 2, 3])

定义多个泛型别名

1
2
3
4
5
6
7
8
9
function genericsFunc<I, O>(arr: I[], func: (arg: I[]) => O): O[] {
const output = func(arr);
return [output];
}

// output: string[]
const output = genericsFunc([1, 3], (arg: number[]): string => {
return arg.toString();
});

指定类型参数

有时候通过自动推断并不能实现预期效果,这时候就要手动添加类型

1
2
3
4
function combine<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}
const arr = combine<number | string>([1, 2], ["a", "b"]);

泛型类

1
2
3
4
5
6
7
8
class Normal<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}

new Normal<number>(2);

泛型约束

有时需要使泛型变量受到某些约束,例如必须存在 length 属性,就需要对泛型进行限制

1
2
3
4
5
6
7
8
function checkLonger<T extends { length: number }>(a: T, b: T): T {
// 若不限制,会报错:类型 T 不存在 length 属性
return a.length > b.length ? a : b;
}
// longer: "wdnmd" | "nmdwsm"
const longer = checkLonger("wdnmd", "nmdwsm");
// 类型“number”的参数不能赋给类型“{ length: number; }”的参数。
// const longer = checkLonger(114, 514);

在泛型约束中使用类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface INormalArg {
label: string;
value: number;
}

function normal<T, K extends keyof T>(arg: T, key: K): T[K] {
return arg[key];
}

const arg: INormalArg = {
label: "baka",
value: 9,
};

// 这里第二个参数只能使用 "label" & "value"
const value = normal(arg, "value");

对 class 类型进行泛型约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Normal {
nFun() {
return "normal";
}
}

class Special extends Normal {
sFun() {
return "special";
}
}

function handle<F extends Normal>(c: new () => F): F {
return new c();
}

const spe = handle(Special);

// normal special
console.log(spe.nFun(), spe.sFun());

编写通用函数准则

  • 可以的话,使用类型本身,而不是对其约束
  • 尽可能少的使用指定类型参数
  • 如果一个类型参数只出现在一个地方,则需要考虑是否真的需要它

其他类型

索引访问类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Normal = {
label: string;
value: number;
};

interface INormal {
label: string;
value: number;
}

// type label = string
type label = Normal["label"];
// type value = number
type value = INormal["value"];

结合 keyof 使用,获取接口或类型别名的所有类型

1
2
3
4
5
6
7
type Normal = {
label: string;
value: number;
};

// type NormalValue = string | number
type NormalValue = Normal[keyof Normal];

结合 typeof 使用,通过特殊类型索引 number, 获取数组子项类型

*原因:数组的默认属性类型即为 number

1
2
3
4
5
6
7
8
9
const normalArray = [
{
label: "baka",
value: 9,
},
];

// type Normal = { label: string; value: number;}
type Normal = typeof normalArray[number];

交叉类型

格式,其中 &交叉运算符

1
type a = b & c

在 typescript 中,可以把类型理解成一系列值的集合,例如:

number:所有数字的集合
对象类型:内部所有属性的集合

而交叉类型 a & b 的结果正式两个集合的交集
当两个类型不存在交集时,得到结果 never

交叉运算符

对于交叉运算符,其满足以下几个特点:

  • 唯一性: A & A 等价于 A
  • 满足交换律: A & B 等价于 B & A
  • 满足结合律: A & B & C 等价于 A & ( b & c )
  • 父类型收敛: 若 B 是 A 的子类型,则 A & B 将被收敛为 B

父类型收敛的几个例子:

1
2
3
4
5
6
// type A0 = 1
type A0 = 1 & number;
// type A1 = "1"
type A1 = "1" & string;
// type A2 = true
type A2 = true & boolean;

特殊: 除了 never 类型外, any 类型与任何类型交叉均为 any 类型

1
2
// type A3 = never
type A3 = any & never

对象类型的交叉

因为需要使得交叉结果均可继承自交叉双方的类型,所以当对象交叉时,结果是这样的

1
2
3
4
5
6
7
8
type Base1 = {
label: string
}
interface Base2 {
value: number
}
// type Normal = { label: string; value: number }
type Normal = Base1 & Base2

当两个对象类型存在相同属性但类型不同时,将会对该属性进行交叉

1
2
3
4
5
6
7
8
9
type Base1 = {
label: string
}
interface Base2 {
label: number
}
// 由于并不存在同事为 string 和 number 的类型,因此 label 的类型变为 never
// type Normal = { label: never }
type Normal = Base1 & Base2

对象类型交叉时尤其注意一点:当类型指定或被推断为具体的字面量类型时,交叉结果将为 never

1
2
3
4
5
6
7
8
9
type Base1 = {
label: 1
}
interface Base2 {
value: 2
}
// 此处的 value 的类型分别为 1 和 2,并非 number 类型
// type Normal = { value: never }
type Normal = Base1 & Base2

特殊:暂未搞懂原因:

当两个对象中存在共有属性,其中一个为 联合类型 且交叉无交集时,交叉后的类型将直接变为 never 类型,其他元素也会消失

其中:由于 boolean 可以看作字面量 true 和 false 的联合类型,因此也拥有上述特性

1
2
3
4
5
6
7
8
type Base1 = {
label: string
}
interface Base2 {
label: 1 | 2
}
// type Normal = never
type Normal = Base1 & Base2

函数类型的交叉

typescript 会利用函数重载的特性来实现不同函数的交叉运算

因此可能会出现如下这种情况

1
2
3
4
5
6
7
8
9
10
11
12
type F1 = (arg1: string, arg2: string) => void
type F2 = (arg1: number, arg2: number) => void
type F3 = F1 & F2
const fn: F3 = (a, b) => {}
/**
* 没有与此调用匹配的重载。
第 1 个重载(共 2 个),“(arg1: string, arg2: string): void”,出现以下错误。
类型“number”的参数不能赋给类型“string”的参数。
第 2 个重载(共 2 个),“(arg1: number, arg2: number): void”,出现以下错误。
类型“string”的参数不能赋给类型“number”的参数。ts(2769)
*/
fn('baka', 9)

且函数类型交叉运算后,相同参数变为联合类型,而返回值类型将做交叉运算

因此将会出现下面这张情况

1
2
3
4
5
6
7
8
type F1 = (arg1: string, arg2: string) => string
type F2 = (arg1: number, arg2: number) => number
type F3 = F1 & F2

const fn: F3 = (a, b) => {
// 必须将返回值断言为 never,否则报错
return '1' as never
}

映射类型

1
type Mapped<K> = { [ P in K ]: T }

P in K 类似 js 中的 for in 语句,用于遍历 K 中的所有类型,T 为你希望该属性所表示的任意类型

添加修饰符

允许使用修饰符,在修饰符前面添加 +- 表示 添加/移除 修饰符,默认为 +

使用 readonly? 修饰符:

1
2
3
4
// type Normal = { readonly x?: number | undefined; readonly y?: number | undefined }
type Normal = { readonly [ K in "x" | "y" ]?: number }
// 将接口的所有属性转换为 只读+必填
type ReadonlyAndRequired<T> = { readonly [ K in keyof T ]-?: T[K] }

as 重新映射

ts 4.1 开始支持,允许对键值重新映射,格式如下

1
type KeyRemapping<T> = [ K in keyof T as NewKeyType ]: T[K]

NewKeyType 必须为 string | number | symbol 的子类型

在对键进行重新映射时,若 as 子句返回 never 类型,该键将被删除

示例:将普通接口类型映射为 getter 格式的类型,即 getXxx: () => Type

1
2
3
4
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };

// type Normal = { getValue: () => number };
type Normal = Getters<{ value: number }>;

其中:

  • `` 为模板字面量类型,后面将会说明
  • Capitalize:将 string 类型转换为首字母大写
  • string & K:利用交叉类型返回 never,过滤非 string 类型

条件类型

类似 js 中的 三元表达式

格式

1
type Type = T extends U ? X : Y;

结合泛型使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Normal = {
label: string;
};

type Hard = {
value: number;
};

type Level<T extends number | string> = T extends number ? Normal : Hard;

function getLevel<T extends number | string>(params: T): Level<T> {
throw "";
}

// const level1: Normal
const level1 = getLevel(9);
// const level2: Hard
const level2 = getLevel("");
// const level3: Normal | Hard
const level3 = getLevel(Math.random() > 0.5 ? 9 : "");

结合 infer 关键字实现类型分发

下面这个案例封装了一个获取函数类型返回值的类型

1
2
3
4
5
6
7
8
type GetReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;

// type Normal = string
type Normal = GetReturnType<() => string>;

其实这就是预定义类型 ReturnType 的实现方法

分布式条件类型

如果被检查的类型,即 T extends U ? X : Y 中的 T 是一个裸类型参数,则该条件类型为分布式条件类型

裸类型参数:没有被数组(T[])、元组([T])或Promise(Promise<T>)包装过

对于分布式条件类型,若传入的被检查的参数为联合类型时,在运算过程中将被分解为多个分支

1
2
3
type Normal = A | B | C extends U ? X : Y
// 等同于
type Normal = (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

下面展示了是否是分布式条件类型时,传入的参数得到的最终结果的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 分布式条件类型
type Normal<T> = T extends boolean ? "Y" : "N"
// type t0 = "N" | "Y"
type t0 = Normal<boolean | number>

// 非分布式条件类型
type WarppedTuple<T> = [T] extends [boolean] ? "Y" : "N"
type WarppedArrary<T> = T[] extends boolean[] ? "Y" : "N"
type WarppedPromise<T> = Promise<T> extends Promise<boolean> ? "Y" : "N"
// type T1 = "N"
type T1 = WarppedTuple<boolean | number>
// type T2 = "N"
type T2 = WarppedArrary<boolean | number>
// type T3 = "N"
type T3 = WarppedPromise<boolean | number>

模板字面量类型

ts 4.1 开始支持,写法类似 js 中的模板字符串,格式如下

其中类型占位符 T 允许为 string | number | boolean | bigint 的子类型

1
type Model<T> = `${T}`

使用示例:

1
2
3
4
type Base<T extends string> = `chiruno is ${ T }`

// type Normal = "chiruno is baka"
type Normal = Base<"baka">

当类型占位符 T 为联合类型时,联合类型会被自动展开
而对于包含多个类型占位符的情况时,多个联合类型将被展开并解析为叉积

1
2
3
4
5
6
7
8
9
// 联合类型自动展开
type Direction = "left" | "top" | "right" | "bottom";
// type CssPadding = "padding-left" | "padding-top" | "padding-right" | "padding-bottom"
type CssPadding = `padding-${Direction}`

// 多个联合类型作叉积
type Concat<T extends string, K extends string> = `${T}-${K}`
// type Hard = "top-true" | "top-false" | "bottom-true" | "bottom-false"
type Hard = Concat<"top" | "bottom", "true" | "false">

模板字面量类型可以将非字符串类型的基本类型字面量转换为对应的字符串类型字面量

1
2
3
4
type ToString<T extends number | string | boolean | bigint> = `${T}`

// type Normal = "baka" | "true" | "9" | "-1234"
type Normal = ToString<9 | "baka" | true | -1234n>

结合 infer条件类型 进行推断

1
2
3
4
5
type Direction = "left" | "top" | "right" | "bottom";
type Prefix<T> = T extends `${ infer R }-${ Direction }` ? R : T;

// type Normal = "margin"
type Normal = Prefix<"margin-left">

预定义类型

ReturnType

使用方式:ReturnType<T>,T 为一个函数类型

获得一个函数类型的返回值类型

1
2
3
4
type Normal = (arg: string) => boolean;

// type Ret = boolean
type Ret = ReturnType<Normal>;

Uppercase & Lowercase & Capitalize & Uncapitalize

分别为 全部大写全部小写首字母大写首字母小写, 仅对 string 类型进行处理的内置类型

1
2
// type Normal = "BAKA"
type Normal = Uppercase<"baka">

声明文件

npm 包的声明文件

与 npm 包绑定的声明文件

在两种情况下,声明文件会和 npm 包绑定

  • 在包目录下存在类型文件 index.d.ts
  • package.json 中显式的定义了 types 属性,指向具体位置的声明文件,如 "types": "index.d.ts"

第三方声明文件

有些时候,npm 包维护者并没有提供声明文件,而由社区中其他人向 @types 贡献了声明文件,可以尝试安装一下对应的 @types

1
npm i @types/[package name] --save-dev

默认情况下,typescript 会自动包含支持全局使用的任何声明定义,如安装 @types/node 后可以在全局使用 process
但如果你不希望被各种全局变量污染作用域,可以在 tsconfig.json 中作如下配置。

1
2
3
4
5
{
"compilerOptions": {
"types": ["node"]
}
}

像这样配置了 types 以后,将只允许使用 node 的 @types 包,即使安装了其他的 @types 包,也不会生效,除非同样将其加入到这里。

自行定义声明文件

若该 npm 包连第三方声明文件都不存在,这时候就只能自行定义声明文件了

  • 创建一个 types 目录,将 marry 的声明文件放到其中,目录为 types/marry/index.d.ts。并配置 tsconfig.json 中的 baseUrlpath 字段,如下。
1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}