2-1、TS 详解

前言

TS 是 JS 的超集:它是完全包含的 JS,并扩展了很多功能
比较重要的扩展功能:

  • 可选静态类型
  • 基于类的面向对象编程

特点:

  • 编写项目维度:
    • 更利于架构维护
    • 支持静态、动态类型监测
      • 但 TS 并不是强类型语言
        • 什么是强类型:不支持任何隐式的转换
  • 代码编译维度:支持自主检测
  • 运行流程维度:编译、检测、产出
  • 复杂特性:支持模块化、泛型、接口……

基础

TS 的里面写的类型,仅仅用于编译时进行检测,编译完成的产物里面不会存在

1
2
3
4
5
// 编译前
const inEable: boolean = true

// 编译后
const inEable = true

基础类型

《基础类型》官方文档:https://typescript.bootcss.com/basic-types.html

与 JS 一样的

boolean、number、string、undefined、null

1
2
3
4
5
const a: boolean = true;
const b: string = "1";
const c: number = 2;
const d: undefined = undefined;
const e: null = null;

与 JS 不一样的

Array - 数组

语法
1
2
3
4
5
6
7
// 从内到外,先写类型,再写 []
const Array1: number[] = [1, 2, 3]

// 从外到内,先写 Array,再写类型 - 数组泛型
const Array2: Array<number> = [1, 2, 3]

const Array3: Array<any> = [1, 2, 3, "32", true];
只读数组
1
2
3
4
5
6
7
8
9
10
11
// 通过 ReadonlyArray<类型> 可以声明只读数组
let Array4: ReadonlyArray<number> = [1, 2, 3, 4]
Array4[0] = 7 // 改值,TS 会报错:类型“readonly number[]”中的索引签名仅允许读取。ts(2542)
Array4.push(4); // 新增,TS 会报错:类型“readonly any[]”上不存在属性“push”。ts(2339)
Array4.length = 40; // 改长度,TS 会报错:无法为“length”赋值,因为它是只读属性。ts(2540)
Array4.splice(0, 1);// 删除,TS 会报错:类型“readonly any[]”上不存在属性“splice”。ts(2339)

let Array5 = [];
Array5 = Array4; // 赋值给其他变量,TS 会报错:类型 "readonly any[]" 为 "readonly",不能分配给可变类型 "any[]"。ts(4104)

Array5 = Array4 as []; // 但可以使用断言重写,使赋值成功
编译结果

没啥特殊的,就是去掉了类型定义

1
2
3
4
5
6
7
// 从内到外,先写类型,再写 []
const Array1 = [1, 2, 3]

// 从外到内,先写 Array,再写类型 - 数组泛型
const Array2 = [1, 2, 3]

const Array3 = [1, 2, 3, "32", true];

Tuple - 元组

一种特殊的数组类型,它允许你指定一个固定长度的数组,且数组中每个位置的元素可以有不同的类型。

语法
1
2
let Tuple: [string, number, boolean];
Tuple = ["Hello", 123, true];
特点
  1. 固定长度: 元组有固定的长度,不能在创建后添加或删除元素。
  2. 类型限制: 元组中的每个位置可以有不同的数据类型,但是一旦定义后,类型不能改变。
场景

函数返回多个值时或者处理一些固定长度的数据结构

编译结果

没啥特殊的,就是去掉了类型定义

1
2
let Tuple;
Tuple = ["Hello", 123, true];

Enum - 枚举

用于定义一组有名字的常量。

语法
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// --- 基本使用,默认情况下,是数值,并且从 0 自增
enum Color1 {
Red, // 等价于 Red = 0
Green, // 等价于 Green = 1
Blue, // 等价于 Blue = 2
}

// 枚举取值:Color.X 或 Color[X]

let myColor1: Color1 = Color1.Red; // 取值时拿的是 = 后面的值
console.log(myColor1); // 0
let myColor1_1: Color1 = Color1['Green']; // 取值时拿的是 = 后面的值
console.log(myColor1_1); // 1

// --- 手动指定数值
enum Color2 {
Red = 3,
Green = 6,
Blue = 9,
}
let myColor2: Color2 = Color2.Red; // 取值时拿的是 = 后面的值
console.log(myColor2); // 3

// --- 手动指定字符串
enum Color3 {
Red = "Red",
Green = "Green",
Blue = "Blue",
}
let myColor3: Color3 = Color3.Red; // 取值时拿的是 = 后面的值
console.log(myColor3); // "Red"

// --- 异构:数值与字符串共存,并且交叉定义,则前一个为数值,后一个数值可不写
enum Color4 {
Red, // 等价于 Red = 0
Green = "Green",
Black, // TS 报错:枚举成员必须具有初始化表达式。ts(1061)
Blue = 9, // 字符串后面的数值必须赋值
Yellow, // 等价于 Yellow = 10(前一个数值 + 1);特殊要求:前一个必须是数值,才可省写数值,否则报错
}
let myColor4: Color4 = Color4.Red; // 取值时拿的是 = 后面的值
console.log(myColor4); // 0
let myColor4_2: Color4 = Color4.Green; // 取值时拿的是 = 后面的值
console.log(myColor4_2); // "Green"
let myColor4_3: Color4 = Color4.Blue; // 取值时拿的是 = 后面的值
console.log(myColor4_3); // 9
let myColor4_4: Color4 = Color4.Yellow; // 取值时拿的是 = 后面的值
console.log(myColor4_4); // 10
反向映射

通过枚举的值,反向拿 key,写法类似于数组下标取值,即 Color[x],x 为枚举定义时某个 key ‘=’ 后面的值
限制:仅支持值为数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Color5 {
Red, // 等价于 Red = 0
Green = "Green",
Blue = 9,
Yellow, // 等价于 Yellow = 10 = 前一个数值 + 1;特殊要求:前一个必须是数值,否则报错
Black = "black",
}
// Color5[x],其中的 x 为枚举定义时某个 key '=' 后面的值

let myColor5 = Color5[0]; // 0 为 Red '=' 后面的值,所以取到 "Red"
console.log(myColor5); // "Red"
let myColor5_2 = Color5["Green"]; // "Green" 就是 key,可以不算反向映射,所以直接取值 "Green"
console.log(myColor5_2); // "Green"
let myColor5_3 = Color5[9]; // 9 为 Blue '=' 后面的值,所以取到 "Blue"
console.log(myColor5_3); // "Blue"
let myColor5_4 = Color5[10]; // 10 为 Yellow '=' 后面的值,所以取到 "Yellow"
console.log(myColor5_4); // "Yellow"
let myColor5_5 = Color5["black"]; // "black" 为 Black '=' 后面的值,按理应该取到 "Black",但 TS 报错
console.log(myColor5_5); // TS 报错:元素隐式具有 "any" 类型,因为索引表达式的类型不为 "number"。ts(7015)
编译结果

枚举经过 TS 的编译后,本质就是对象

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
28
29
30
31
32
33
var Color5;
(function (Color5) {
Color5[Color5["Red"] = 0] = "Red";
Color5["Green"] = "Green";
Color5[Color5["Blue"] = 9] = "Blue";
Color5[Color5["Yellow"] = 10] = "Yellow";
})(Color5 || (Color5 = {}));
// Color5[x],其中的 x 为枚举定义时某个 key '=' 后面的值
let myColor5 = Color5[0]; // 0 为 Red '=' 后面的值,所以取到 "Red"
console.log(myColor5); // "Red"
let myColor5_2 = Color5["Green"]; // "Green" 为 Green '=' 后面的值,所以取到 "Green"
console.log(myColor5_2); // "Green"
let myColor5_3 = Color5[9]; // 9 为 Blue '=' 后面的值,所以取到 "Blue"
console.log(myColor5_3); // "Blue"
let myColor5_4 = Color5[10]; // 10 为 Yellow '=' 后面的值,所以取到 "Yellow"
console.log(myColor5_4); // "Yellow"

// -------- 补充说明 ------
// 枚举经过 TS 的编译后,本质就是普通的对象
// 通过该对象的 key 就能明白为啥支持[反向映射与其限制]
Color5 = {
'0': 'Red',
'9': 'Blue',
'10': 'Yellow',
Red: 0,
Green: 'Green',
Blue: 9,
Yellow: 10,
Black: 'black'
}

Color5[Color5["Red"] = 0] = "Red";
// 这段代码会执行两次赋值 Color5["Red"] = 0; Color5[0] = "Red"; 双 key 赋值
面试题
  • 手写实现一个枚举
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
// 枚举原始数据(异构的)
enum Color6 {
Red, // 等价于 Red = 0
Green = "Green",
Blue = 9,
Yellow, // 等价于 Yellow = 10
Black = "black",
}

// 其实答案就是枚举的编译结果
let Enum;
(function (Enum) {
Enum["Red"] = 0;
Enum["0"] = "Red";

Enum["Green"] = "Green";

Enum["Blue"] = 9;
Enum["9"] = "Blue";

Enum["Yellow"] = 10;
Enum["10"] = "Yellow";

Enum["Black"] = "black";
})(Enum || (Enum = {}));

Any

表示任意类型,可以赋值给任何变量,该变量后续可直接使用,并且该变量可以赋值给其他类型变量,any 变量以后将跳过类型检测

1
2
3
4
5
6
7
8
9
10
let anyValue: any = 1;

// 重新赋值其他类型,TS 不会报错
anyValue = "xxsw";

// 直接调用该值的方法,TS 不会报错
console.log(anyValue.slice(1)); // xsw

// 将 any 类型赋值其他类型,TS 不会报错
let numberValue: number = anyValue;
使用场景
  • JS 迁移到 TS:使用 any 可以快速推进,不用陷入无穷的类型之中,但仅仅是过渡
  • 不关心传参类型的函数:比如有个自定义打印函数,里面就一段console.log('自定义打印:', params),这时候任何值传进来都直接打印,所以可以给 params 设为 any,因为不关心它具体类型

Unknown

表示未知类型(是 any 的安全版本),可以赋值给任何变量,该变量后续不可直接使用,需要进行类型检测与类型断言后才能使用,并且该变量不可以赋值给其他非unknown或any类型变量

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

let unknownnValue: unknown = 1;

// 重新赋值其他类型,TS 不会报错
unknownnValue = "xxsw";

// 直接调用该值的方法,TS 会报错:“unknownnValue”的类型为“未知”。ts(18046)
// console.log(unknownnValue.slice(1));

if (typeof unknownnValue === "string") {
// 类型判断后,再调用该值的方法,TS 不会报错
console.log(unknownnValue.slice(1)); // xsw
}

// 类型断言后,再调用该值的方法,TS 不会报错
console.log((unknownnValue as string).slice(1)); // xsw

// 将 unknown 类型赋值其他类型,TS 会报错:不能将类型“unknown”分配给类型“string”。ts(2322)
let stringValue: string = unknownnValue;

// 将 unknown 类型赋值 any 类型,TS 不会报错
let newAnyValue: any = unknownnValue;

// 将 unknown 类型赋值 unknown 类型,TS 不会报错
let newUnknownnValue: unknown = unknownnValue;
使用场景

任何使用 any 的地方,都应该优先使用 unknown,这样使用时更安全(因为要先类型推断)

Void

表示没有类型

  • 用于函数:表明没有返回值或者返回 undefined
  • 用于变量:表明只能赋值为 undefined 和 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 主动声明 void
function voidFunction(): void {
console.log("This is a console.log");
}

// 未写 return,则为 void
function noReturnFunction() {
console.log("This is a console.log");
}

// 有 return,但后面未写值,也为 void
function onlyReturnFunction() {
console.log("This is a console.log");
return;
}

let voidValue1: void = 1; // TS 会报错:不能将类型“number”分配给类型“void”。ts(2322)
let voidValue2: void; // TS 不会报错
let voidValue3: void = undefined; // TS 不会报错
为什么无返回值的函数,其返回值类型为 void 而不是 undefined?

因为更符合直觉,void 表示函数无返回值。
虽然无返回值的函数默认返回的是 undefined,但 undefined 本身具有一些歧义:代表该值未定义、属性不存在,若将无返回值的函数类型定为 undefined,则将直面歧义。

Never

永远不存在的值的类型,是所有类型的子类型
它可以赋值给任何类型,但其他类型(除了 Never)不能赋值给它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum errorLevel {
A,
Z,
}

function throwError(level: errorLevel) {
if (level === errorLevel.A) {
throw new Error("这个错误是可控抛出的");
} else if (level === errorLevel.Z) {
throw new Error("这个错误是不可控抛出的");
} else {
let a = level; // 此处 a 的类型被推断为 never,因为永远到不了这

// 将其他类型赋值给 never,TS 报错:不能将类型“string”分配给类型“never”。ts(2322)
a = "b";

// 将 never 赋值给其他类型,TS 不会报错
const c: number = a;
}
}
使用场景
  • 异常兜底
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
28
29
30
31
32
33
34
// 之前的逻辑,TS 不会报错
enum errorLevel {
A,
Z,
}

function throwError(level: errorLevel) {
if (level === errorLevel.A) {
throw new Error("这个错误是可控抛出的");
} else if (level === errorLevel.Z) {
throw new Error("这个错误是不可控抛出的");
} else {
let a = level; // 此处 a 的类型被推断为 never,因为永远到不了这
// 等价于 let a:never = level; 并且不会报错
}
}

// 若新增了枚举值,但下面未处理
enum errorLevel {
A,
B, // 新增了 B
Z,
}

function throwError(level: errorLevel) {
if (level === errorLevel.A) {
throw new Error("这个错误是可控抛出的");
} else if (level === errorLevel.Z) {
throw new Error("这个错误是不可控抛出的");
} else {
let a: never = level; // 此处 TS 会报错:不能将类型“errorLevel_2”分配给类型“never”
// 因为未处理 B 的判断,所以代码到这里了,发现将其他类型赋值给 never,则 TS 自动就报错了
}
}
  • 抛错函数
1
2
3
4
// 因为这个函数会抛出异常,所以也不会返回的
function crashFunc(): never {
throw new Error('this function will crash')
}

Objetc - 对象

TS 里面有三个对象:object | Object | {}

object:JS 中没有;在 TS 中表示非原始类型,本质是一种类型
1
function fn(o: object): void {}
Object:JS/TS 中都表示 Object 构造函数,本身具有很多属性与方法
1
2
3
4
// Object.create()
// Object.assign()
// Object.keys()
// 等等...
{}:JS 中表示基于 Object 的实例;在 TS 中特指没有成员的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const emptyObject = {}; // 等价于 const emptyObject = new Object()

// 若新增属性/方法,则 TS 会报错:类型“{}”上不存在属性“name”/“getName”。ts(2339)
emptyObject.name = "1";
emptyObject.getName = () => {};

// 成功的处理报错方案
const emptyObject2 = {} as { name: string; getName: Function }; // 新增类型断言后

// 若再新增属性/方法,则 TS 不会报错
emptyObject2.name = "1";
emptyObject2.getName = () => {};

// 错误的处理报错方案
const emptyObject2: { name: string; getName: Function } = {};
// 新增类型后,若还为 {}
// 则 TS 报错:类型“{}”缺少类型“{ name: string; getName: Function; }”中的以下属性: name, getNamets(2739)

类型断言

当你清楚的知道某个值的类型时,可以进行断言,告知 TS:“相信我,我知道自己在干什么”。
断言只是在编译阶段起作用。

写法 1 - 尖括号

1
2
let str: any = 'xxx dsad dsad'
const len: number = (<string>str).length

写法 2 - as 语法

1
2
let str: any = 'xxx dsad dsad'
const len: number = (str as string).length

两种写法是等价的,但在 TS 使用 JSX 时,as 才被允许

! - 用于除去 null 和 undefined 的

identifier!identifier的类型里去除了nullundefined

1
2
3
4
5
6
7
let A: string | undefined;
A!.slice(1);

let B: string = A!;
// 这里必须加 !,否则TS 报错:
// 不能将类型“string | undefined”分配给类型“string”。
// 不能将类型“undefined”分配给类型“string”。ts(2322)

进阶类型

接口 - interface

《接口》官方文档:https://typescript.bootcss.com/interfaces.html
定义:对行为的抽象

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const person: person = {
name: 'xx',
age: 20,
sayHello(){
console.log('你好')
}
}

// interface 定义
interface person {
name: string;
age: number;
sayHello(): void;
}

进阶

只读 - readonly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface person {
readonly name: string; // 只读
age: number;
sayHello(): void;
}

const person: person = {
name: 'xx',
age: 20,
sayHello(){
console.log('你好')
}
}

person.name = "lisi"; // TS 报错:无法为“name”赋值,因为它是只读属性。ts(2540)
可选 - ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface person {
readonly name: string; // 只读
age?: number; // 可选属性,表明该属性可以不存在
sayHello(): void;
}

const person: person = {
name: 'xx',
// age: 20, // person 里面没有 age 属性时,TS 并不会报错
sayHello(){
console.log('你好')
}
}

person.age = 20; // 为 age 赋 number 类型,TS 不会报错
person.age = '20'; // 为 age 赋 string 类型,TS 会报错:不能将类型“string”分配给类型“number”。ts(2322)
可索引类型 - [key:T]: X

表示当使用 T 类型去索引时会返回 X 类型的值
支持字符串、数字、symbol三种索引
字符串、数字共存时,则要求数字索引值的类型必须为字符串索引值的子类型
因为使用数字索引,JS 底层还是会转为字符串索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface person {
readonly name: string; // 只读
age?: number;
sayHello(): void;
[key: string]: any; // 索引为 string 类型,值为 any 类型
[key: number]: number; // 索引为 number 类型,值为 number 类型
[key: symbol]: symbol; // 索引为 symbol 类型,值为 symbol 类型
}

// 不报错,符合索引,索引为 string 类型,值为 any
person.address = "四川省";

// 不报错,符合索引,索引为 number 类型,值为 number
person[1] = 1;

// 不报错,符合索引,索引为 symbol 类型,值为 symbol
person[Symbol()] = Symbol("我是唯一的");

// 报错,不符合索引,索引为 number 类型,值要为 number
person[1] = "四川省"; // TS 报错:不能将类型“string”分配给类型“number”。ts(2322)

// 报错,不符合索引,索引为 symbol 类型,值要为 symbol
person[Symbol()] = 123; // TS 报错:不能将类型“number”分配给类型“symbol”。ts(2322)

当使用了字符串索引时,其他属性的定义都要跟字符串索引保持一致

1
2
3
4
5
interface person {
[key: sting]: string;
name: string; // 可以,跟字符串索引一致
age: number; // 不可以,TS 会报错,值为 number,跟字符串索引的值 string 类型不一致
}

索引可以设置为只读

1
2
3
4
5
interface person {
readonly [key: sting]: string;
}
const person: person = { name: 'lisi' }
person.name = 'zhangsan' // TS 会报错:无法为“name”赋值,因为它是只读属性。ts(2540)
继承
接口继承接口

接口可以互相继承
语法:接口名 extends 接口名1,[接口名2, 接口名3, ...]
一个接口可以继承多个接口,创建出多个接口的合成接口。

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
interface Animal {
name: string;
age: number;
}

// 单继承
interface Cat extends Animal {
eatFish(): void;
}

// 多继承
interface JuCat extends Cat, Animal {
isFat(): boolean;
}

// 实现
let XiaoJuCat = {} as JuCat;
XiaoJuCat.name = "XiaoJu";
XiaoJuCat.age = 3;
XiaoJuCat.eatFish = () => {
console.log("[ 每天四顿,每顿 2 条鱼 ] >");
};
XiaoJuCat.isFat = () => {
console.log("[ XiaoJu 非常非常的胖 ] >");
return true;
};
接口继承类

没错,接口是可以继承的,但只会继承类的成员,但不会继承具体实现,并且该接口只能被这个类或其子类所实现(implement)。
语法:接口名 extends 类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() {}
}

class TextBox extends Control {}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() {}
}

class Location {}

在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。
Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上,SelectableControl就像Control一样,并拥有一个select方法。ButtonTextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但ImageLocation类并不是这样的。

定义函数类型

interface 可以将定义类型作用于函数
定义函数类型时,也可以混合其他类型哦

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
28
29
30
31
interface MyselfDesc {
// 函数自身类型:包含参数列表和返回值类型
(name: string, age: number): string;

// 函数自身的属性
name: string;

// 函数自身的方法
sayName(): string
}

// 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配,仅要求对应位置上的参数类型是兼容的
const myselfDesc: MyselfDesc = (n: string, a: number): string => {
return `你好,我是${n},今年${a}岁,很高兴认识你`;
};
myselfDesc.name = 'myselfDesc'
myselfDesc.sayName = function() {
return this.name
}

// 由于 myselfDesc 已经被定义为 MyselfDesc ,则可以简化去掉的类型定义,TS 会自动推断的
const myselfDesc: MyselfDesc = (n, a) => {
return `你好,我是${n},今年${a}岁,很高兴认识你`;
};

// 但如果返回值的类型跟定义的不一致,则 TS 会报错
// 不能将类型“(n: string, a: number) => number”分配给类型“MyselfDesc”。
// 不能将类型“number”分配给类型“string”。ts(2322)
const myselfDesc2: MyselfDesc = (n, a) => {
return 123;
};
定义类类型

interface 可以将定义类型作用于
语法:class 类名 implements 接口名
接口描述了类的公共部分,而不是公共和私有两部分。它不会帮你检查类是否具有某些私有成员。
并且接口的类型最终会落到实例上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// PersonInterface 负责描述
interface PersonInterface {
name: string; // 属性描述
age: number; // 属性描述
say(msg: string): void; // 方法描述
}

// Person 负责实现
class Person implements PersonInterface {
name = ""; // 属性实现
age = 0; // 属性实现
say(msg: string) {
// 方法实现
console.log("[ msg ] >", msg);
}
constructor(name: string, age: number) {}
}

const p1 = new Person('lisi', 30) // 实例 p1 的类型为 PersonInterface

编译结果

经过 TS 编译后,interface啥都没有,不会在结果代码里面留东西

泛型

《泛型》官方文档:https://typescript.bootcss.com/generics.html
考虑重用性而提出的,适用于多种类型

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个传啥返回啥的函数

// 1. 不加类型时,TS 会提醒:参数“arg”隐式具有“any”类型。ts(7006)
function onlyReturn(arg) {
return arg
}

// 若给它加类型,那加什么类型呢?number?string?都不太符合,因为功能传啥返回啥,所以不是固定类型
// 所以需要 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function onlyReturn<T>(arg: T): T {
return arg
}
// T 就是我们声明的类型变量,它会自动捕获类型
// 并且 onlyReturn 被称为 泛型函数
// 与普通函数的区别只是:多了个 <T>

约定成俗:
T、U、K:键值
V:纯值
E:节点

进阶

泛型接口
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
// 泛型函数接口
interface OnlyReturn1 {
<T>(arg: T): T
}

const onlyReturn: OnlyReturn1 = <T>(arg: T): T => {
return arg
}

// 泛型接口,使用 <> 括起泛型类型,跟在接口名后面
interface OnlyReturn2<T> {
(arg: T): T
}

// 当使用 OnlyReturn2 时,必须传入类型参数来指定泛型类型

// 可以是:number
const onlyReturn: OnlyReturn2<number> = <T>(arg: T): T => {
return arg
}

// 也可以是:string
const onlyReturn: OnlyReturn2<string> = <T>(arg: T): T => {
return arg
}
泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 泛型类,使用 <> 括起泛型类型,跟在类名后面
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T): => T
}

// 当使用时,必须传入类型参数来指定泛型类型

// 可以是:number
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = (x, y) => x + y

// 可以是:string
let myGenericNumber = new GenericNumber<string>()
myGenericNumber.zeroValue = 'name'
myGenericNumber.add = (x, y) => x + y
泛型约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这个例子想使用 .length 时,TS 会报错,因为传入的类型可能没有 .length
const onlyReturn = <T>(arg: T): T => {
console.log(arg.length); // Error: T doesn't have .length
return arg
}

// 这种情况我们可以对传入的类型进行约束,确保一定有 length 属性

// 新增一个约束接口
interface Length {
length: number
}

// 使用约束接口:<T extends Length>
const onlyReturn = <T extends Length>(arg: T): T => {
console.log(arg.length); // TS 不会报错
return arg
}
// 但使用约束后,将不适用于任意类型了
在泛型约束中使用类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 该函数功能:用属性名从对象里获取这个属性的值
const getObjValue = <T, K>(obj: T, key: K) {
return obj[key]
}

// 上述情况下,可能 obj 里面不存在传入 key,这可以加强约束
const getObjValue = <T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getObjValue(x, "a"); // okay
getObjValue(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

高级类型

《高级类型》官方文档:https://typescript.bootcss.com/advanced-types.html

交叉类型

定义:将多个类型合并为一个类型,该类型必须满足所有多类型
白话:多个类型取
语法:类型1 & 类型2 & ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface P {
name: string;
}
interface E {
age: number;
}

// OO 必须满足 P & E 类型
const OO: P & E = {
name: "liusan",
age: 20,
};

// 存在冲突的,合并结果用:且
interface P {
name: string;
}
interface E {
name: number;
}
type PE = P & E // 冲突出来的 name 类型为: never(string & number)

联合类型

定义:将多个类型合并为一个类型,该类型只需满足多类型之一
白话:多个类型取
语法:类型1 | 类型2 | ...

1
2
3
4
5
6
7
8
9
10
11
interface P {
name: string;
}
interface E {
age: number;
}

// OO 只需满足 P、E 类型之一
const OO: P | E = {
name: "liusan",
};
类型保护

当使用联合类型时,只能确定该类型包含多类型的交集成员,如下代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
interface Bird {
fly(): void;
layEggs(): void;
}

interface Fish {
swim(): void;
layEggs(): void;
}

function getSmallPet(): Fish | Bird {
const result = {} as Fish | Bird;

return result;
}

let pet = getSmallPet();
pet.layEggs(); // okay

// TS 报错:
// 类型“Fish | Bird”上不存在属性“swim”。
// 类型“Bird”上不存在属性“swim”。ts(2339)
pet.swim();

// TS 报错:
// 类型“Fish | Bird”上不存在属性“fly”。
// 类型“Fish”上不存在属性“fly”。ts(2339)
pet.fly();

// 针对于这种情况,如果改成这样写,TS 还是报错
if (pet.swim) {
pet.swim();
} else if (pet.fly) {
pet.fly();
}

// 为了使代码工作,就只能多次使用类型断言,麻烦
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
} else {
(<Bird>pet).fly();
}

// 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet的类型的话就好了。
// 这就可以借用 TS 的类型保护机制

类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类型保护函数定义
const isFish = (pet: Fish | Bird): pet is Fish => {
return (<Fish>pet).swim !== undefined;
}

// 例子中的 pet is Fish 就是类型谓词
// 谓词形式:parameterName is Type,parameterName 必须是当前函数的参数名

// 类型保护函数使用
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}

// 使用 类型保护 后,TS 知道在 if 分支里 pet 是 Fish 类型;
// 并且它还知道在 else 分支里,一定不是 Fish 类型,一定是 Bird 类型。
编译结果

TS 编译后,会留下对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function isFish(pet) {
return pet.swim !== undefined;
}

// 例子中的 pet is Fish 就是类型谓词
// 谓词形式:parameterName is Type,parameterName 必须是当前函数的参数名

// 使用 类型保护
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}

类型别名 - type

作用:给一个类型起个新名字
类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Name = string;
type GetName = () => string;
type NameOrGetName = Name | GetName;

const getName = (n: NameOrGetName): Name => {
if (typeof n === "string") {
return n;
} else {
return n();
}
};

// 支持泛型
type Tree<T> = {
value: T;
left: Tree<T>; // 支持属性里引用自己
right: Tree<T>; // 支持属性里引用自己
};
映射类型

一种从旧类型中创建新类型的一种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Person {
name: string;
age: number
}

type Readonly<T> = {
readonly [K in keyof T]: T[K];
}

type Partial<T> = {
[K in keyof T]?: T[K];
}

// 新类型为:Person 所有的变为只读
type PersonReadonly = Readonly<Person>

// 新类型为:Person 所有的变为可选
type PersonPartial = Readonly<Person>

复杂的映射类型

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
28
29
// 代理函数 proxify 的基础 TS 实现
type Proxy<T> = {
get(): T;
set(value: T): void;
};

type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
};

function proxify<T>(o: T): Proxify<T> {
const result = {} as Proxify<T>;

for (const key in o) {
if (Object.prototype.hasOwnProperty.call(o, key)) {
result[key] = {
get() {
return o[key];
},
set(value) {
o[key] = value;
},
};
}
}

return result;
}
let proxyProps = proxify({ name: "123", age: 1 });
编译结果

经过 TS 编译后,interface啥都没有,不会在结果代码里面留东西

装饰器

《装饰器》官方文档:https://typescript.bootcss.com/decorators.html
装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。
基础语法:@expression,expression 求值后必须为函数,将在运行时被调用
使用的前提是:tsconfig.json 开启配置

1
2
3
4
5
{
"compilerOptions" : {
"experimentalDecorators": true
}
}

装饰器工厂

定制装饰器用的,本质是一个返回函数的函数

1
2
3
4
5
6
// 装饰器工厂示例代码
function Get(path: string) {
return function(target, propertyKey: string, decriptor: PropertyDescriptor){
// 定制的业务代码 ...
}
}
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
28
29
30
31
32
33
34
35
36
37
38
39
// 多个装饰器使用示例代码

// 装饰器工厂 1 示例代码
function Get(path: string) {
console.log('Get()')
return function(target, propertyKey: string, decriptor: PropertyDescriptor){
// 定制的业务代码 ...
console.log('Get() do something')
}
}

// 装饰器工厂 2 示例代码
function Post(path: string) {
console.log('Post()')
return function(target, propertyKey: string, decriptor: PropertyDescriptor){
// 定制的业务代码 ...
console.log('Post() do something')
}
}

class UserService {
userName = "lisi";

@Get('/userinfo')
@Post('/userinfo')
userinfo() {
// ...
}
}

// 上述代码的装饰器执行逻辑为:
// Get()
// Post()
// Post() do something
// Get() do something

// 多个装饰器的执行逻辑是:
// 1、先从上往下执行(得到装饰器的返回函数);
// 2、再从下往上执行(执行返回函数的调用)
编译结果

上述代码经过 TS 编译后,将变成以下 JS 代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};

// 多个装饰器使用示例代码

// 装饰器工厂 1 示例代码
function Get(path) {
console.log("Get()");
return function (target, propertyKey, decriptor) {
console.log("Get() do something");
// 定制的业务代码 ...
};
}
// 装饰器工厂 2 示例代码
function Post(path) {
console.log("Post()");
return function (target, propertyKey, decriptor) {
console.log("Post() do something");
// 定制的业务代码 ...
};
}
class UserService {
constructor() {
this.userName = "lisi";
}
userinfo() {
// ...
}
}
__decorate(
[Get("/userinfo"), Post("/userinfo")],
UserService.prototype,
"userinfo",
null
);

Reflect.decorate:ECMAScript 2016 标准引入的一个方法,主要用于处理装饰器

装饰器分类

类装饰器

作用于类上的装饰器,可以用来监视,修改或替换类定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类装饰器
// 参数仅 1 个:
// constructor 类的构造函数
function Sealed(constructor: Function) {
// 密封
Object.seal(constructor)
Object.seal(constructor.prototype)
}

@Sealed
class UserService2 {
userName = "lisi";

userinfo() {
// ...
}
}
编译结果
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
28
29
30
31
32
33
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};

function Sealed(constructor) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
let UserService2 = class UserService2 {
constructor() {
this.userName = "lisi";
}
userinfo() {
// ...
}
};
UserService2 = __decorate([Sealed], UserService2);
实际场景
  1. 重载构造函数
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 类装饰器
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
// 类装饰器有返回值,则会使用该构造函数来替换类的声明。
return class extends constructor { // extends 语法,用来处理原来的原型链
newProperty = "new property";
hello = "override";
};
}

@classDecorator
class Greeter {
property = "property";

hello: string;

constructor(m: string) {
this.hello = m;
}
}

console.log(new Greeter("world"));
// Greeter {
// property: 'property',
// hello: 'override',
// newProperty: 'new property'
// }

// 编译结果:

// __decorate 未变,不赘述
var __decorate =
(this && this.__decorate) ||
...

function classDecorator(constructor) {
return class extends constructor {
constructor() {
super(...arguments);
this.newProperty = "new property";
this.hello = "override";
}
};
}

let Greeter = class Greeter {
constructor(m) {
this.property = "property";
this.hello = m;
}
};

Greeter = __decorate([classDecorator], Greeter);
console.log(new Greeter("world"));
  1. 针对老业务进行扩展时,很有用

假如有个场景:之前有个 class,有点复杂,也满足之前的需求。但现在需要加几个属性与方法来支持新业务,则这种情况该怎么处理?

  1. 侵入式:直接改这个 class,强行添加新的属性与方法
  2. 继承式:通过 extends 继承这个类,然后再添加新的属性与方法

以上两个方法都能解决问题,但都存在对原有类的冲击,容易产生不可预测的影响

  1. 可以通过类装饰器进行可插拔的更改,并且不会对原来的类产生影响

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原来的类
class Course {
constructor() {}
}

// 采用 类装饰器 扩展
function Version_2_1(constructor: Function) {
constructor.prototype.startClass = function () {
console.log("[ let‘s startClass ] >", this.teacher);
};
}

@Version_2_1
class Course {
teacher = "lisi";
constructor() {}
}

const course = new Course();
course.startClass(); // [ let‘s startClass ] > lisi
方法装饰器

作用于方法的装饰器,可以用来监视,修改或替换方法定义。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 方法装饰器工厂
// 参数:可自定义
function Post(path: string) {
console.log("Post()");

// 返回函数参数:3 个:
// traget:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey:成员的名字
// propertyDescriptor:成员的属性描述符(若小于ES5,该会是undefined)
return function (
target: any,
propertyKey: string,
decriptor: PropertyDescriptor
) {
console.log("Post() do something");

// 定制的业务代码 ...
decriptor.enumerable = true; // 修改属性描述符的 enumerable 属性值为 true

// 打印参数
console.log("[ target ] >", JSON.stringify(target));
console.log("[ propertyKey ] >", propertyKey);
console.log("[ decriptor ] >", decriptor);
};
}

class UserService {
userName = "lisi";

@Post("/userinfo")
userinfo() {
// ...
}
}

// 打印结果:
// Post()
// Post() do something
// [ target ] > {}
// [ propertyKey ] > userinfo
// [ decriptor ] > {
// value: [Function: userinfo],
// writable: true,
// enumerable: true,
// configurable: true
// }
访问器装饰器

作用于访问器(geter/seter)的装饰器,可以用来监视,修改或替换访问器定义。
不允许同时装饰一个成员的 get 和 set 访问器

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 访问器装饰器

// 参数:可自定义
function configurable(value: boolean) {
// 返回函数参数:3 个:
// traget:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey:成员的名字
// propertyDescriptor:成员的属性描述符(若小于ES5,该会是undefined)
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}

class Point {
_x: number;
_y: number;

constructor(x: number, y: number) {
this._x = x;
this._y = y;
}

@configurable(false)
get x(): number {
return this._x + 1;
}

@configurable(false)
get y(): number {
return this._y + 1;
}
}

const point = new Point(0, 0);
console.log("[ point ] >", point);
console.log("[ point.x ] >", point.x);
console.log("[ point.y ] >", point.y);

// 打印结果:
// [ point ] > Point { _x: 0, _y: 0 }
// [ point.x ] > 1
// [ point.y ] > 1
编译结果
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
28
29
30
31
32
// __decorate 未变,不赘述
var __decorate =
(this && this.__decorate) ||
...

function configurable(value) {
return function (target, propertyKey, descriptor) {
descriptor.configurable = value;
};
}
class Point {
constructor(x, y) {
this._x = x;
this._y = y;
}
get x() {
return this._x + 1;
}
get y() {
return this._y + 1;
}
}
__decorate([
configurable(false)
], Point.prototype, "x", null);
__decorate([
configurable(false)
], Point.prototype, "y", null);
const point = new Point(0, 0);
console.log("[ point ] >", point);
console.log("[ point.x ] >", point.x);
console.log("[ point.y ] >", point.y);
属性装饰器

作用于属性的装饰器,可以用来监视,修改或替换属性定义。
一般用于处理这个属性的元数据

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
import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
return function (target: any, porpertyKey: string) {
return Reflect.metadata(formatMetadataKey, formatString);
};
}

function getFormat(target: any, porpertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, porpertyKey);
}

class Greeter2 {
@format("Hello, %s")
greeting: string;

constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
实际场景

属性拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function greetingWraper(target: any, porpertyKey: string) {
Object.defineProperty(target, porpertyKey, {
// 劫持操作 ...
});
}

class Greeter2 {
@greetingWraper
greeting: string = "111";

constructor(message: string) {
this.greeting = message;
}
}
参数装饰器

作用于参数的装饰器,可以用来监视参数是否传入

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const requiredMetadataKey = Symbol("required");

// 参数装饰器
// 参数 3 个:
// target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey:成员的名字
// index:参数在函数参数列表中的索引
function required(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey
);
}

function validate(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
let method = descriptor.value;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName
);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (
parameterIndex >= arguments.length ||
arguments[parameterIndex] === undefined
) {
throw new Error("Missing required argument.");
}
}
}

return method!.apply(this, arguments);
};
}

class GreeterX {
greeting: string;

constructor(message: string) {
this.greeting = message;
}

@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}

元数据

官方文档:元编程 - JavaScript | MDN

总结

了解完装饰器后,如果是经常写 nodejs 的人就会发现已经不知不觉用了装饰器

1
2
3
4
5
6
7
// 某个 xxx.controller.ts
@ApiOperation({ summary: '用户登录' })
@ApiCreatedResponse({ type: UserEntity })
@Post('login')
async login(@Body() loginDto: LoginDto) {
// 登录的业务逻辑....
}

其中的@ApiOperation、@ApiCreatedResponse、@Post、@Body等,就是很多 nodejs 框架所提供的装饰器
所以装饰器的实际使用场景在nodejs比较广泛,客户端领域可能用的比较少。

TS 原理解析

1
2
3
4
5
6
7
8
9
// TS 源码
let a: number = 1

// TS 做的事情:
// 1. scanner:扫描器,扫描并识别代码与生成数据流(Token)
// 2. parser:解析器, 生成语法树(AST)
// 3. binder:绑定器,基于语法树创建 symbols,进行类型绑定(判断每个节点用什么类型去检查)
// 4. checker:检查器,检查 TS 代码的语法问题
// 5. emitter:发射器,根据每个节点的检查结果,翻译为 js

扩展知识

Object.seal()

官方文档:Object.seal() - JavaScript | MDN
作用:密封一个对象。
密封后:该对象将【不能添加新属性】、【不能删除现有属性或更改其可枚举性和可配置性】、【不能重新分配其原型】、【现有属性的值是可以更改的】(这是与 freeze 的区别)

1
2
3
4
5
6
7
8
const obj = { name: 'zhangsan' }
const sealObj = Object.seal(obj)
sealObj === obj // true

sealObj.age = 12 // 新增属性失败,可能不会报错,age 属性并不会添加进去
delete sealObj.name // 删除属性失败,可能不会报错,name 属性并不会删除
sealObj.name = 'lisi' // 更改现有属性成功
obj.__proto__ = {} // 更改原型失败,报错:Uncaught TypeError: #<Object> is not extensible(可扩展)

Object.freeze()

官方文档:Object.freeze() - JavaScript | MDN
作用:冻结一个对象
冻结后:该对象将【不能添加新的属性】、【不能移除现有的属性】、【不能更改它们的可枚举性、可配置性、可写性或值】、【对象的原型也不能被重新指定】

1
2
3
4
5
6
7
8
const obj = { name: 'zhangsan' }
const sealObj = Object.freeze(obj)
sealObj === obj // true

sealObj.age = 12 // 新增属性失败,可能不会报错,age 属性并不会添加进去
delete sealObj.name // 删除属性失败,可能不会报错,name 属性并不会删除
sealObj.name = 'lisi' // 更改现有属性失败,可能不会报错,name 属性并不能更改
obj.__proto__ = {} // 更改原型失败,报错:Uncaught TypeError: #<Object> is not extensible(可扩展)

面试题

interface VS type

interface type
extends 可以 不可以
implements 可以 不可以
联合|交叉 不可以 可以

使用情况:如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。


2-1、TS 详解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/2-1、TS 详解/
作者
黄智强
发布于
2024年1月13日
许可协议