TypeScript 笔记

[[toc]]

代码库

点击去往

环境搭建

1
2
3
4
5
6
7
8
9
10
# 安装
npm install typescript -g

tsc xx.ts # 生成 js 文件
tsc -d xx.ts #生成 xx.d.ts文件
node xx.js # 运行 js 文件

# 方便直接运行 ts 文件的包
npm install ts-node -g
ts-node xx.ts # 直接运行 ts 文件

体验一下 ts

类型注解(annotation)

  • 我们告诉 typescript 变量是什么类型
1
2
let a: number; // annotation
a = 11;
  • 声明数字
1
let a: number = 10;
  • 声明数组
1
2
3
let a: number[] = [1, 2, 3]; // 声明数字数组
let b: string[] = ["1", "2", "3"]; // 声明字符串数组
let c: Array<number> = [1, 2, 3]; //运用泛型声明数组
  • 声明对象
1
const obj: { name: string; age: number } = { name: "李四", age: 19 };
  • 函数
1
2
3
4
5
6
7
function fn(): void {
// 没有返回值
}

function fn2(): number {
return 1;
}
  • 结构赋值
1
2
3
function sum({ a, b }: { a: number; b: number }): number {
return a + b;
}
  • 任意类型
1
2
let num: any = "1";
num = 2;
  • 声明数组对象
1
2
3
4
5
6
7
8
interface Obj {
name: string;
age: number;
}
let arr: Obj[] = [
{ name: "张安", age: 18 },
{ name: "荒芜", age: 20 },
];

类型推断(inference)

  • typescript 会自动的判断变量的类型
1
let a = 123; // 鼠标放上 a 去看

never 类型的理解

  • 一个无法执行完成的函数的返回值可以是 never
1
2
3
4
5
6
function fn(): never {
throw new Error();
}
function fn(): never {
while (true) {}
}

必看(运行环境)

使用的编辑器 vscode

typescript 版本 4.2.4

tsconfig.json 配置

:::tip

笔记的全部代码都是开启严格模式,没开严格模式的地方会特别说明

:::

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"outDir": "./dist",
"strict": true, // 开启严格模式
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["dist"],
"include": ["src"]
}

基础

数据类型

  1. any类型除了不能赋值给never与其他所有类型兼容

  2. unknow可以被其他所有类型赋值

  3. anyunknow外,void不可赋值给其他类型

:::tip

strict 严格模式在 tsc -init 之后的 tsconfig.json 里面设置

:::

  1. 不开启strict选项时,void可被nullundefinednever`赋值

  2. 开启strict选项时,void可被undefinednever赋值

ts数据类型

定义变量

JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。

原始数据类型包括:布尔值、数值、字符串、nullundefined 以及 ES6 中的新类型 SymbolBigInt

null 和 undefined 是所有类型的子类型 也就是说 undefined 类型的变量,可以赋值给 number 类型的变量 (在没有开启严格模式的情况下)

1
2
3
4
5
6
// 冒号后面的玩意叫类型注解
let num: number = 123;
let flag: boolean = false;
let str: string = "hello ts";
let n: null = null;
let un: undefined = undefined;

定义函数

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

1
2
3
4
5
6
7
function fn(): void {
console.log("我莫得返回值");
}

function fn2(): string {
return "我的返回值是 string";
}

声明一个 void 的变量只能被赋值成 null 和 undefined (在没有开启严格模式的情况下 null 才能 赋值给 void 变量)

1
2
3
let v: void = null; // "strict": false
let v1: void = null; // "strict": true;Type 'null' is not assignable to type 'void'.ts(2322)
let v: void = undefined;

:::tip

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

:::

1
2
3
4
5
6
7
let returnStr: (str: string) => string = function (str) {
return str;
};
type Str = (str: string) => string;
let fn: Str = function (str) {
return str;
};

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理

类似于 java 里面的方法重载

例如我们定义一个函数名为 add 的函数,当传入的值是 number 类型的时候我们就相加,传入的是 string 类型的时候我们就转换成数字再相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add(x1: number, x2: number): number;
function add(x1: string, x2: string): string;

function add(
x1: number | string,
x2: number | string,
): number | string | undefined {
if (typeof x1 == "number" && typeof x2 == "number") {
return x1 + x2;
} else if (typeof x1 == "string" && typeof x2 == "string") {
return x1 + x2;
}
}
let result = add(1, 2);
console.log(result);

定义任意值 any

任意值(Any)用来表示允许赋值为任意类型。

简单理解如果你把一个变量注解成了 any 那么你写的 ts 代码就变成了 js 了

1
2
3
let a: any = "123";
a = 5;
a = false;

等价于

1
2
3
var a = "123";
a = 5;
a = false;

类型推论 Type Inference

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

也就是说编译器会帮你推断一个类型

1
2
let num = 1; // 鼠标移动到 num 上会看到 vscode 给你推断出了number
num = "123"; // Type 'string' is not assignable to type 'number'.ts(2322)

以上代码等价于

1
2
let num: number = 1;
num = "123";

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论

::: warning

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

:::

联合类型 Union Types

联合类型(Union Types)表示取值可以为多种类型中的一种。

联合类型使用 | 分隔每个类型。

案例

1
2
3
4
// hello 变量既可以是 number 类型也可以是 string 类型
let hello: number | string;
hello = 123; // 此时是 number 类型 只能访问 number 里面的方法
hello = "123"; // 此时是 string 类型 只能访问 string 里面的方法

访问联合类型的属性或方法

:::warning

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

:::

1
2
3
4
5
6
7
8
9
// 访问联合类型的共有属性
function getLength1(s: string | number): number {
return s.length; //Property 'length' does not exist on type 'string | number'.Property 'length' does not exist on type 'number'.ts(2339)
// number 没有 length 这个方法所以报错
}
function getLength2(s: string | number): string {
return s.toString();
// string 和 number 里面都有 toString 方法所以没有报错
}

对象的类型——接口 Interface

什么是接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

案例
1
2
3
4
5
6
7
8
9
10
11
12
// 接口名称一般大写
interface Person {
name: string;
age: number;
}

let obj: Person = { name: "张安", age: 18 };

let arr: Person[] = [
{ name: "张安", age: 18 },
{ name: "荒芜", age: 20 },
];

以上的案例我们发现接口定义之后用里面定义的 name 和 age 在 obj 里面都得完完整整的写完 那么我不想写完呢?或者只写一个呢?

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性

{4}
1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
name: string;
age: number;
gender?: string;
}

let obj: Person = { name: "张安", age: 18 };

let arr: Person[] = [
{ name: "张安", age: 18 },
{ name: "荒芜", age: 20, gender: "男" },
];

那么我想让这个 interface 里面的属性可以任意添加呢?

任意属性

有时候我们希望一个接口允许有任意的属性,可以使用如下方式

{6,17-19}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Person {
name: string;
age: number;
gender?: string;
// propName 不是固定的写法,可以随便写 比如你写个 aaa 什么的
[propName: string]: any;
}

let obj: Person = { name: "张安", age: 18 };

let arr: Person[] = [
{ name: "张安", age: 18 },
{
name: "荒芜",
age: 20,
gender: "男",
p1: "我是任意属性",
p2: 123,
p3: "我是任意属性",
},
];

::: warning

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

:::

{6}
1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age: number; // Property 'age' of type 'number' is not assignable to string index type 'string'.ts(2411)
gender?: string; // Property 'gender' of type 'string | undefined' is not assignable to string index type 'string'.ts(2411)
// 注意看注解成了 string
[propName: string]: string;
}

let obj: Person = { name: "张安", age: 18 }; //Type '{ name: string; age: number; }' is not assignable to type 'Person'.Property 'age' is incompatible with index signature.Type 'number' is not assignable to type 'string'.ts(2322)

// 上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。

那么我想让 interface 里面的属性一开始就被赋值并且只能被读取不能被修改呢?

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

{2,11}
1
2
3
4
5
6
7
8
9
10
11
interface Person {
readonly id: number;
name: string;
age: number;
gender?: string;
// propName 不是固定的写法,可以随便写 比如你写个 aaa 什么的
[propName: string]: any;
}

let obj: Person = { id: 1, name: "张安", age: 18 };
obj.id = 2; // Cannot assign to 'id' because it is a constant or a read-only property.

类型断言 Type Assertion

语法

1
// 值 as 类型 或 <类型>值  一般用前者

类型断言的用途

简单理解就是,现在你比编译器更懂现在的这个变量是什么类型

1
2
3
4
5
6
7
function getValue(value: number | string): void {
// 在没有明确传入的类型的时候我们只能调用联合类型中的公共属性和方法
// 明确告诉 ts 我知道他肯定传入的是 number 然后调用 toFixed
let n = (value as number).toFixed(2);
console.log(n);
}
getValue(1);

确定赋值断言

用处告诉 Ts 该属性被明确的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
x = 10;
}

进阶

类型别名 type

类型别名用来给一个类型起个新名字

1
2
3
4
5
6
7
type Obj = {
name: string;
age: number;
gender?: string;
};

let obj: Obj = { name: "张安", age: 18, gender: "男" };

:::tip

类型别名常用于联合类型

:::

元组 Tuple

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

元组定义成啥样就要验证遵守定义的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr1: [number, string] = [1, "hello"];
console.log(arr1);

// 对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项
let arr2: [boolean, string];
arr2[0] = false; // Variable 'arr2' is used before being assigned
arr2[1] = "1"; // Variable 'arr2' is used before being assigned

arr2 = [false, "12"];

// 如果数组越界直接 push 注解的类型没有注解的类型无法添加
arr1.push(1);
arr1.push(false);

枚举 Enum

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

1
2
3
4
5
6
7
8
9
enum Days {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
}

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Days {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
}

console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

枚举可以手动赋值

{2,6-7}
1
2
3
4
5
6
7
8
9
10
11
12
enum Day {
Sun = 10,
Mon,
Tue,
Wed,
Thu = "123",
Fri = 9,
Sat,
}
console.log(Day.Sun); //10
console.log(Day.Tue); //12
console.log(Day.Sat); //10

泛型 Generics

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

例子

1
2
3
4
5
6
7
// 创建一个函数,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
function createArray(length: number, value: any): any[] {
return new Array(length).fill(value);
}

let result = createArray(5, "123"); // [ '123', '123', '123', '123', '123' ]
console.log(result);

:::tip

思考 : 返回的是一个任意类型的数组,若是我后面直接调用 result[0].toFixed(2) 字符串上明明没有这个方法我强行调用不就报错了? 还有一个问题,我们输入 result[0]. 点时候在 vscode 中的代码提示没有了.我们写 ts 可以说就是为了良好的代码提示这时提示没有了是否意味着我们写错了?

此时我们需要一个工具我们给 value 输入什么类型 createArray 函数就返回什么类型的数组,并且 result 会被类型推断为正确的类型

此时泛型就可以登场了

:::

{6}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个函数,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
function createArray<T>(length: number, value: T): T[] {
return new Array(length).fill(value);
}

let result = createArray(5, "123"); //鼠标放上去看类型推断,试着把 "123" 修改成不同的数据类型
console.log(result[0]);

// 写法二

let createArray2 = function <T>(length: number, value: T): Array<T> {
return new Array(length).fill(value);
};

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法

1
2
3
4
// 用泛型创建一个函数返回传入字符串的 length
function getLength<T>(str: T): T {
return str.length; // 此时报错 str 上不存在 length 属性
}

此时我们就需要告诉编译器 str 上肯定存在 length 属性,我们就需要使用到泛型约束

{12}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 用泛型创建一个函数返回传入字符串的 length
interface LengthI {
length: number;
}
// 泛型 T 继承 LengthI 让传入数据类型必须有 length 属性
function getLength<T extends LengthI>(str: T): T {
console.log(str.length);
return str;
}

let result = getLength("123");
let result2 = getLength(123); // Argument of type 'number' is not assignable to parameter of type 'LengthI'.
console.log(result);

泛型接口

1
2
3
4
5
6
7
8
// 泛型接口
interface CreateArray {
<T>(length: number, value: T): Array<T>;
}

let createArray: CreateArray = function <T>(length: number, value: T): T[] {
return new Array(length).fill(value);
};

我们可以把泛型参数提前到接口名上

1
2
3
4
5
6
7
8
9
10
11
// 泛型接口
interface CreateArray<T> {
<T>(length: number, value: T): Array<T>;
}

let createArray: CreateArray<any> = function <T>(
length: number,
value: T,
): T[] {
return new Array(length).fill(value);
};

一些总结

高级类型

  • 交叉类型,使用类型操作符 &

  • 联合类型,使用类型操作符

  • 索引类型,使用类型操作符 keyofT[K]

  • 映射类型,使用类型操作符 in

  • 条件类型,使用类型操作符 T extends U ? X : Y infer

  • 另外还有用于类型保护的类型谓词中的 is 操作符以及上面所介绍的 typeof

高级类型

参考资料