# 一份通俗易懂的 TS 教程,入门 + 实战
| TypeScript | JavaScript |
|---|---|
| JavaScript 的超集,用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页。 |
| 强类型,支持静态和动态类型 | 动态弱类型语言 |
| 可以在编译期间发现并纠正错误 | 只能在运行时发现错误 |
| 不允许改变变量的数据类型 | 变量可以被赋予不同类型的值 |
# 基础类型
# boolean、number 和 string 类型
# undefined 和 null 类型
默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。
但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。
# any、unknown 和 void 类型
- any
- 不清楚用什么类型,可以使用 any 类型。这些值可能来自于动态的内容,比如来自用户输入或第三方代码库
- 不建议使用 any,不然就丧失了 TS 的意义
- unknown 类型(代表任何类型)
- 不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?可以使用 unknown 类型
- unknown 类型代表任何类型,它的定义和 any 定义很像,但是它是一个安全类型,使用 unknown 做任何事情都是不合法的。
比如,这样一个 divide 函数,
function divide(param: any) {
return param / 2;
}
2
3
把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。
把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险,如下图
function divide(param: unknown) {
return param / 2;
}
2
3
因为不知道 param 的类型,使用运算符 /,导致报错
再配合类型断言,即可解决这个问题,
function divide(param: unknown) {
return param as number / 2;
}
2
3
- void
- void 类型与 any 类型相反,它表示没有任何类型。
# never 类型
类型表示的是那些永不存在的值的类型。
never 类型是任何类型的子类型,也可以赋值给任何类型。
没有类型是 never 的子类型,没有类型可以赋值给 never 类型(除了 never 本身之外)。即使 any 也不可以赋值给 never 。
- 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行。
- 函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步。
# 数组类型

- 数组里的项写错类型会报错
- push 时类型对不上会报错
如果数组想每一项放入不同数据怎么办?用元组类型
# 元组类型
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let tuple: [number, string] = [18, "lin"];
- 写错类型会报错
- 越界会报错
- 可以对元组使用数组的方法,比如使用 push 时,不会有越界报错
# 函数类型
定义函数类型需要定义输入参数类型和输出类型(输出类型也可以忽略,因为 TS 能够根据返回语句自动推断出返回值类型。)。
# 函数表达式写法
let add2 = (x: number, y: number): number => {
return x + y;
};
2
3
# 可选参数
参数后加个问号,代表这个参数是可选的
WARNING
注意可选参数要放在函数入参的最后面,不然会导致编译错误。
function add(x: number, y: number, z?: number): number {
return x + y;
}
add(1, 2, 3);
add(1, 2);
2
3
4
5
6
# 函数赋值
JS 中变量随便赋值没问题,
let fn = functoin() {}
fn = '123'
2
3
# 函数重载
# 不同参数类型
比如我们实现一个 add 函数,如果传入参数都是数字,就返回数字相加,如果传入参数都是字符串,就返回字符串拼接,
function add(x: number[]): number
function add(x: string[]): string
function add(x: any[]): any {
if (typeof x[0] === 'string') {
return x.join()
}
if (typeof x[0] === 'number') {
return x.reduce((acc, cur) => acc + cur)
}
}
2
3
4
5
6
7
8
9
10
在 TS 中,实现函数重载,需要多次声明这个函数,前几次是函数定义,列出所有的情况,最后一次是函数实现,需要比较宽泛的类型,比如上面的例子就用到了 any。
# 不同参数个数
假设这个 add 函数接受更多的参数个数,比如还可以传入一个参数 y,如果传了 y,就把 y 也加上或拼接上,就可以这么写,
function add(x: number[]): number
function add(x: string[]): string
function add(x: number[], y: number[]): number
function add(x: string[], y: string[]): string
function add(x: any[], y?: any[]): any {
if (Array.isArray(y) && typeof y[0] === 'number') {
return x.reduce((acc, cur) => acc + cur) + y.reduce((acc, cur) => acc + cur)
}
if (Array.isArray(y) && typeof y[0] === 'string') {
return x.join() + ',' + y.join()
}
if (typeof x[0] === 'string') {
return x.join()
}
if (typeof x[0] === 'number') {
return x.reduce((acc, cur) => acc + cur)
}
}
console.log(add([1,2,3])) // 6
console.log(add(['lin', '18'])) // 'lin,18'
console.log(add([1,2,3], [1,2,3])) // 12
console.log(add(['lin', '18'], ['man', 'handsome'])) // 'lin,18,man,handsome'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
其实写起来挺麻烦的,后面了解泛型之后写起来会简洁一些,不必太纠结函数重载,知道有这个概念即可,平时一般用泛型来解决类似问题。
# interface
# interface 基本概念
interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
定义 interface 一般首字母大写,代码如下
interface Person {
name: string
age: number
}
const p1: Person = {
name: 'lin',
age: 18
}
2
3
4
5
6
7
8
9
- 属性必须和类型定义的时候完全一致
- 少写了属性,报错:
- 多写了属性,报错:
interface
interface 不是 JS 中的关键字,所以 TS 编译成 JS 之后,这些 interface 是不会被转换过去的,都会被删除掉,interface 只是在 TS 中用来做静态检查。
# 可选属性
跟函数的可选参数是类似的,在属性上加个 ?,这个属性就是可选的,比如下面的 age 属性
interface Person {
name: string
age?: number
}
const p1: Person = {
name: 'lin',
}
2
3
4
5
6
7
8
# 只读属性
如果希望某个属性不被改变,可以这么写:
interface Person {
readonly id: number
name: string
age: number
}
2
3
4
5
改变这个只读属性时会报错
# interface 描述函数类型
interface 也可以用来描述函数类型
interface ISum {
(x: number, y: number): number;
}
const add: ISum = (num1, num2) => {
return num1 + num2;
};
2
3
4
5
6
7
# 自定义属性(可索引的类型)
上文中,属性必须和类型定义的时候完全一致,如果一个对象上有多个不确定的属性,怎么办?
interface RandomKey {
[propName: string]: string;
}
const obj: RandomKey = {
a: "hello",
b: "lin",
c: "welcome",
};
2
3
4
5
6
7
8
9
如果把属性名定义为 number 类型,就是一个类数组了,看上去和数组一模一样
interface LikeArray {
[propName: number]: string;
}
const arr: LikeArray = ["hello", "lin"];
arr[0]; // 可以使用下标来访问值
2
3
4
5
6
7
当然,不是真的数组,数组上的方法它是没有的
# duck typing
interface 还有一个响亮的名称:duck typing(鸭子类型)。
TIP
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
-- James Whitcomb Riley
interface FunctionWithProps {
(x: number): number
name: string
}
2
3
4
FunctionWithProps 接口描述了一个函数类型,还向这个函数类型添加了 name 属性,这看上去完全是四不像,但是这个定义是完全可以工作的。
const fn: FunctionWithProps = (x) => {
return x;
};
fn.name = "hello world";
2
3
4
5
# 类
JS 是靠原型和原型链来实现面向对象编程的,es6 新增了语法糖 class。
TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。
# 类基本写法
定义一个 Person 类,有属性 name 和 方法 speak
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
speak() {
console.log(`${this.name} is speaking`);
}
}
const p1 = new Person("lin"); // 新建实例
p1.name; // 访问属性和方法
p1.speak();
2
3
4
5
6
7
8
9
10
11
12
13
14
# 继承
使用 extends 关键字实现继承,定义一个 Student 类继承自 Person 类。
class Student extends Person {
study() {
console.log(`${this.name} needs study`);
}
}
const s1 = new Student("lin");
s1.study();
2
3
4
5
6
7
8
9
继承之后,Student 类上的实例可以访问 Person 类上的属性和方法。
# super 关键字
注意,上例中 Student 类没有定义自己的属性,可以不写 super ,但是如果 Student 类有自己的属性,就要用到 super 关键字来把父类的属性继承过来。
比如,Student 类新增一个 grade(成绩) 属性,就要这么写:
class Student extends Person {
grade: number;
constructor(name: string, grade: number) {
super(name);
this.grade = grade;
}
}
const s1 = new Student("lin", 100);
2
3
4
5
6
7
8
9
不写 super 会报错
# 多态
子类对父类的方法进行了重写,子类和父类调同一个方法时会不一样。
class Student extends Person {
speak() {
return `Student ${super.speak()}`;
}
}
2
3
4
5
# public
public,公有的,一个类里默认所有的方法和属性都是 public。
比如上文中定义的 Person 类,其实是这样的:
class Person {
public name: string
public constructor(name: string) {
this.name = name
}
public speak() {
console.log(`${this.name} is speaking`)
}
}
2
3
4
5
6
7
8
9
public 可写可不写,不写默认也是 public。
# private
private,私有的,只属于这个类自己,它的实例和继承它的子类都访问不到。
将 Person 类的 name 属性改为 private。
class Person {
private name: string
public constructor(name: string) {
this.name = name
}
public speak() {
console.log(`${this.name} is speaking`)
}
}
2
3
4
5
6
7
8
9
实例访问 name 属性,会报错:
# protected
protected 受保护的,继承它的子类可以访问,实例不能访问。
将 Person 类的 name 属性改为 protected。
class Person {
protected name: string
public constructor(name: string) {
this.name = name
}
public speak() {
console.log(`${this.name} is speaking`)
}
}
2
3
4
5
6
7
8
9
# static
static 是静态属性,可以理解为是类上的一些常量,实例和子类都不能访问。
比如一个 Circle 类,圆周率是 3.14,可以直接定义一个静态属性。
class Circle {
static pi: 3.14
public radius: number
public constructor(radius: number) {
this.radius = radius
}
public calcLength() {
return Circle.pi * this.radius * 2 // 计算周长,直接访问 Circle.pi
}
}
2
3
4
5
6
7
8
9
10
# 抽象类
抽象类,听名字似乎是非常难理解的概念,但其实非常简单。
TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。
其实 TS 还对 JS 扩展了一个新概念——抽象类。
所谓抽象类,是指只能被继承,但不能被实例化的类,就这么简单。
- 抽象类有两个特点:
- 抽象类不允许被实例化
- 抽象类中的抽象方法必须被子类实现
abstract class Animal {}
const a = new Animal(); // 报错
2
3
# 抽象类中的抽象方法必须被子类实现
abstract class Animal {
constructor(name:string) {
this.name = name
}
public name: string
public abstract sayHi():void
}
class Dog extends Animal {
constructor(name:string) {
super(name)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
定义一个 Dog 类,继承自 Animal 类,但是却没有实现 Animal 类上的抽象方法 sayHi,报错,
正确的用法如下,
abstract class Animal {
constructor(name:string) {
this.name = name
}
public name: string
public abstract sayHi():void
}
class Dog extends Animal {
constructor(name:string) {
super(name)
}
public sayHi() {
console.log('wang')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 抽象方法和多态
多态是面向对象的三大基本特征之一
多态指的是,父类定义一个抽象方法,在多个子类中有不同的实现,运行的时候不同的子类就对应不同的操作
# this 类型
# 链式调用
类的成员方法可以直接返回一个 this,这样就可以很方便地实现链式调用
class StudyStep {
step1() {
console.log("listen");
return this;
}
step2() {
console.log("write");
return this;
}
}
const s = new StudyStep();
s.step1().step2(); // 链式调用
2
3
4
5
6
7
8
9
10
11
12
13
14
# 灵活调用子类父类方法
在继承的时候,this 可以表示父类型,也可以表示子类型
class StudyStep {
step1() {
console.log("listen");
return this;
}
step2() {
console.log("write");
return this;
}
}
class MyStudyStep extends StudyStep {
next() {
console.log("before done, study next!");
return this;
}
}
const m = new MyStudyStep();
m.step1()
.next()
.step2()
.next(); // 父类型和子类型上的方法都可随意调用
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这样就保持了父类和子类之间接口调用的连贯性
# interface 和 class 的关系
interface 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
interface 同样可以用来约束 class,要实现约束,需要用到 implements 关键字。
# implements
implements 是实现的意思,class 实现 interface。
interface MusicInterface {
playMusic(): void;
}
class Cellphone implements MusicInterface {
playMusic() {}
}
2
3
4
5
6
7
定义了约束后,class 必须要满足接口上的所有条件,不写就会报错
# 处理公共的属性和方法
手机还有打电话的功能,就可以这么做,Cellphone 类 implements 两个 interface。
# 约束构造函数和静态属性
使用 implements 只能约束类实例上的属性和方法,要约束构造函数和静态属性,需要这么写。
interface CircleStatic {
new (radius: number): void
pi: number
}
const Circle:CircleStatic = class Circle {
static pi: 3.14
public radius: number
public constructor(radius: number) {
this.radius = radius
}
}
2
3
4
5
6
7
8
9
10
11
12
未定义静态属性 pi,会报错
# 枚举
# 基本使用
enum Direction {
Up,
Down,
Left,
Right
}
2
3
4
5
6
枚举成员会被赋值为从 0 开始递增的数字,
console.log(Direction.Up); // 0
console.log(Direction.Down); // 1
console.log(Direction.Left); // 2
console.log(Direction.Right); // 3
2
3
4
枚举会对枚举值到枚举名进行反向映射,
console.log(Direction[0]); // Up
console.log(Direction[1]); // Down
console.log(Direction[2]); // Left
console.log(Direction[3]); // Right
2
3
4
如果枚举第一个元素赋有初始值,就会从初始值开始递增
enum Direction {
Up = 6,
Down,
Left,
Right
}
console.log(Direction.Up) // 6
console.log(Direction.Down) // 7
console.log(Direction.Left) // 8
console.log(Direction.Right) // 9
2
3
4
5
6
7
8
9
10
11
# 反向映射的原理
枚举是如何做到反向映射的呢,我们不妨来看一下被编译后的代码
var Direction;
(function(Direction) {
Direction[(Direction["Up"] = 6)] = "Up";
Direction[(Direction["Down"] = 7)] = "Down";
Direction[(Direction["Left"] = 8)] = "Left";
Direction[(Direction["Right"] = 9)] = "Right";
})(Direction || (Direction = {}));
2
3
4
5
6
7
# 手动赋值
enum ItemStatus {
Buy = 1,
Send,
Receive
}
console.log(ItemStatus['Buy']) // 1
console.log(ItemStatus['Send']) // 2
console.log(ItemStatus['Receive']) // 3
2
3
4
5
6
7
8
9
但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。
比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写,
enum ItemStatus {
Buy = 100,
Send = 20,
Receive = 1
}
console.log(ItemStatus['Buy']) // 100
console.log(ItemStatus['Send']) // 20
console.log(ItemStatus['Receive']) // 1
2
3
4
5
6
7
8
9
# 计算成员
枚举中的成员可以被计算,比如经典的使用位运算合并权限,可以这么写,
enum FileAccess {
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
}
console.log(FileAccess.Read) // 2 -> 010
console.log(FileAccess.Write) // 4 -> 100
console.log(FileAccess.ReadWrite) // 6 -> 110
2
3
4
5
6
7
8
9
# 字符串枚举
字符串枚举的意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
// do something
}
2
3
4
5
6
7
8
9
10
11
# 常量枚举
上文的例子,使用 const 来定义一个常量枚举
const enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
// do something
}
2
3
4
5
6
7
8
9
10
11
12
13
编译出来的 JS 代码会简洁很多,提高了性能。
const value = "UP";
if (value === "UP" /* Up */) {
// do something
}
2
3
4
不写 const 编译出来是这样的,
var Direction;
(function(Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
const value = "UP";
if (value === Direction.Up) {
// do something
}
2
3
4
5
6
7
8
9
10
11
# 类型推论
# 定义时不赋值
let a;
a = 18;
a = "lin";
2
3
4
定义时不赋值,就会被 TS 自动推导成 any 类型,之后随便怎么赋值都不会报错。
# 初始化变量
因为赋值的时候赋的是一个字符串类型,所以 TS 自动推导出 userName 是 string 类型。
# 设置默认参数值
定义一个打印年龄的函数,默认值是 18
function printAge(num = 18) {
console.log(num);
return num;
}
2
3
4
那么 TS 会自动推导出 printAge 的入参类型,传错了类型会报错。
# 决定函数返回值
# 最佳通用类型
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。比如,
let arr = [0, 1, null, "lin"];
# 内置类型
# JS 八种内置类型
let name: string = "lin";
let age: number = 18;
let isHandsome: boolean = true;
let u: undefined = undefined;
let n: null = null;
let obj: object = { name: "lin", age: 18 };
let big: bigint = 100n;
let sym: symbol = Symbol("lin");
2
3
4
5
6
7
8
# ECMAScript 的内置对象
比如,Array、Date、Error 等,
const nums: Array<number> = [1, 2, 3];
const date: Date = new Date();
const err: Error = new Error("Error!");
const reg: RegExp = /abc/;
Math.pow(2, 9);
2
3
4
5
6
7
8
9
# DOM 和 BOM
比如 HTMLElement、NodeList、MouseEvent 等
# 高级类型(一)
# 联合类型
如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。
例如,一个变量既支持 number 类型,又支持 string 类型,就可以这么写:
let num: number | string;
num = 8;
num = "eight";
2
3
4
联合类型大大提高了类型的可扩展性,但当 TS 不确定一个联合类型的变量到底是哪个类型的时候,只能访问他们共有的属性和方法。
比如这里就只能访问 number 类型和 string 类型共有的方法,如下图,
如果直接访问 length 属性,string 类型上有,number 类型上没有,就报错了
# 交叉类型
如果要对对象形状进行扩展,可以使用交叉类型 &。
比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性,就可以这么写,
interface Person {
name: string
age: number
}
type Student = Person & { grade: number }
2
3
4
5
6
这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性,
TIP
联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。
# 类型别名(type)
就像我们项目中配置 alias,不用写相对路径就能很方便地引入文件
import componentA from '../../../../components/componentA/index.vue'
// 变成
import componentA from '@/components/componentA/index.vue
2
3
类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。
比如下面这个例子,getName 这个函数接收的参数可能是字符串,可能是函数,就可以这么写。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver; // 联合类型
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
2
3
4
5
6
7
8
9
10
TIP
类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string; // 基本类型
type arrItem = number | string; // 联合类型
const arr: arrItem[] = [1, "2", 3];
type Person = {
name: Name,
};
type Student = Person & { grade: number }; // 交叉类型
type Teacher = Person & { major: string };
type StudentAndTeacherList = [Student, Teacher]; // 元组类型
const list: StudentAndTeacherList = [
{ name: "lin", grade: 100 },
{ name: "liu", major: "Chinese" },
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# type 和 interface 的区别
比如下面这个例子,可以用 type,也可以用 interface。
interface Person {
name: string
age: number
}
const person: Person = {
name: 'lin',
age: 18
}
type Person = {
name: string
age: number
}
const person: Person = {
name: 'lin',
age: 18
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 两者相同点:
- 都可以定义一个对象或函数
- 都允许继承
type addType = (num1: number, num2: number) => number;
interface addType {
(num1: number, num2: number): number;
}
// 这两种写法都可以定义函数类型
const add: addType = (num1, num2) => {
return num1 + num2;
};
2
3
4
5
6
7
8
9
10
// interface 继承 interface
interface Person {
name: string;
}
interface Student extends Person {
grade: number;
}
const person: Student = {
name: "lin",
grade: 100,
};
// type 继承 type
type Person = {
name: string,
};
type Student = Person & { grade: number }; // 用交叉类型
// interface 继承 type
type Person = {
name: string,
};
interface Student extends Person {
grade: number;
}
// type 继承 interface
interface Person {
name: string;
}
type Student = Person & { grade: number }; // 用交叉类型
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
interface 使用 extends 实现继承, type 使用交叉类型实现继承
- 两者不同点:
- interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
- type 是类型别名,用于给各种类型定义别名,让 TS 写起来更简洁、清晰。
- type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行
- interface 可以合并重复声明,type 不行
// 重复声明 type ,就报错了
interface Person {
name: string;
}
interface Person {
// 重复声明 interface,就合并了
age: number;
}
const person: Person = {
name: "lin",
age: 18,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
// 报错
type Person = {
name: string,
};
type Person = {
// Duplicate identifier 'Person'
age: number,
};
const person: Person = {
name: "lin",
age: 18,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
# 类型保护
// 如果有一个 getLength 函数,入参是联合类型 number | string,返回入参的 length,此时会报错
function getLength(arg: number | string): number {
return arg.length;
}
2
3
4
5
改造一下
function getLength(arg: number | string): number {
if (typeof arg === "string") {
return arg.length;
} else {
return arg.toString().length;
}
}
2
3
4
5
6
7
# 类型断言
类型断言语法:
值 as 类型
使用类型断言来告诉 TS,我(开发者)比你(编译器)更清楚这个参数是什么类型,你就别给我报错了
function getLength(arg: number | string): number {
const str = arg as string
if (str.length) {
return str.length
} else {
const number = arg as number
return number.toString().length
}
}
2
3
4
5
6
7
8
9
TIP
注意,类型断言不是类型转换,把一个类型断言成联合类型中不存在的类型会报错。
// 报错
function getLength(arg: number | string): number {
return (arg as number[]).length
}
2
3
4
# 字面量类型
有时候,我们需要定义一些常量,就需要用到字面量类型,比如,
type ButtonSize = "mini" | "small" | "normal" | "large";
type Sex = "男" | "女";
const sex: Sex = "不男不女";
2
3
4
5
# 泛型
TIP
软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。
WARNING
写 any 类型不好,毕竟在 TS 中尽量不要写 any
# 泛型基本使用
泛型的语法是 <> 里写类型参数,一般可以用 T 来表示
function print<T>(arg: T): T {
console.log(arg);
return arg;
}
2
3
4
这样,我们就做到了输入和输出的类型统一,且可以输入输出任何类型。
TIP
泛型的写法对前端工程师来说是有些古怪,比如 <> T ,但记住就好,只要一看到 <>,就知道这是泛型。
print <string> "hello"; // 定义 T 为 string
print("hello");
2
3
我们知道,type 和 interface 都可以定义函数类型,也用泛型来写一下,type 这么写:
type Print = <T>(arg: T) => T;
const printFn: Print = function print(arg) {
console.log(arg);
return arg;
};
2
3
4
5
interface 这么写:
interface Iprint<T> {
(arg: T): T;
}
function print<T>(arg: T) {
console.log(arg);
return arg;
}
const myPrint: Iprint<number> = print;
2
3
4
5
6
7
8
9
10
# 默认参数
如果要给泛型加默认参数,可以这么写:
interface Iprint<T = number> {
(arg: T): T;
}
function print<T>(arg: T) {
console.log(arg);
return arg;
}
const myPrint: Iprint = print;
2
3
4
5
6
7
8
9
10
# 处理多个函数参数
现在有这么一个函数,传入一个只有两项的元组,交换元组的第 0 项和第 1 项,返回这个元组。
function swap(tuple) {
return [tuple[1], tuple[0]];
}
2
3
这么写,我们就丧失了类型,用泛型来改造一下。
我们用 T 代表第 0 项的类型,用 U 代表第 1 项的类型。
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
2
3
# 函数副作用操作
我们希望调用 API 都清晰的知道返回类型是什么数据结构,就可以这么做
interface UserInfo {
name: string
age: number
}
function request<T>(url:string): Promise<T> {
return fetch(url).then(res => res.json())
}
request<UserInfo>('user/info').then(res =>{
console.log(res)
})
2
3
4
5
6
7
8
9
10
11
12
# 约束泛型
假设现在有这么一个函数,打印传入参数的长度,我们这么写:
// 会报错
function printLength<T>(arg: T): T {
console.log(arg.length);
return arg;
}
2
3
4
5
那么现在我想约束这个泛型,一定要有 length 属性,怎么办?
可以和 interface 结合,来约束类型。
interface ILength {
length: number
}
function printLength<T extends ILength>(arg: T): T {
console.log(arg.length)
return arg
}
2
3
4
5
6
7
8
这其中的关键就是
<T extends ILength>,让这个泛型继承接口 ILength,这样就能约束泛型。
我们定义的变量一定要有 length 属性,比如下面的 str、arr 和 obj,才可以通过 TS 编译。
const str = printLength("lin");
const arr = printLength([1, 2, 3]);
const obj = printLength({ length: 10 });
2
3
# 泛型的一些应用
使用泛型,可以在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。
# 泛型约束类
定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,就可以这么写:
class Stack<T> {
private data: T[] = []
push(item:T) {
return this.data.push(item)
}
pop():T | undefined {
return this.data.pop()
}
}
// 给 pop 方法定义 static 关键字,就报错了
2
3
4
5
6
7
8
9
10
在定义实例的时候写类型,比如,入栈和出栈都要是 number 类型,就这么写:
const s1 = new Stack<number>()
这样,入栈一个字符串就会报错
WARNING
特别注意的是,泛型无法约束类的静态成员。
# 泛型约束接口
使用泛型,也可以对 interface 进行改造,让 interface 更灵活。
interface IKeyValue<T, U> {
key: T
value: U
}
const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}
2
3
4
5
6
7
# 泛型定义数组
定义一个数组,我们之前是这么写的:
const arr: number[] = [1, 2, 3];
// 现在这么写也可以:
const arr: Array<number> = [1, 2, 3];
2
3
4
5
泛型(Generics),从字面上理解,泛型就是一般的,广泛的。
# 高级类型(二)
# 索引类型
从对象中抽取一些属性的值,然后拼接成数组
const userInfo = {
name: "lin",
age: "18",
};
function getValues(userInfo: any, keys: string[]) {
return keys.map((key) => userInfo[key]);
}
// 抽取指定属性的值
console.log(getValues(userInfo, ["name", "age"])); // ['lin', '18']
// 抽取obj中没有的属性:
console.log(getValues(userInfo, ["sex", "outlook"])); // [undefined, undefined]
2
3
4
5
6
7
8
9
10
11
12
13
虽然 obj 中并不包含 sex 和 outlook 属性,但 TS 编译器并未报错
此时使用 TS 索引类型,对这种情况做类型约束,实现动态属性的检查。
TIP
理解索引类型,需先理解 keyof(索引查询)、T[K](索引访问) 和 extends (泛型约束)。
# keyof(索引查询)
keyof 操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface IPerson {
name: string;
age: number;
}
type Test = keyof IPerson; // 'name' | 'age'
2
3
4
5
6
# T[K](索引访问)
T[K],表示接口 T 的属性 K 所代表的类型,
interface IPerson {
name: string;
age: number;
}
let type1: IPerson['name'] // string
let type2: IPerson['age'] // number
2
3
4
5
6
7
# extends (泛型约束)
T extends U,表示泛型变量可以通过继承某个类型,获得某些属性,之前讲过
# 检查动态属性
对索引类型的几个概念了解后,对 getValue 函数进行改造,实现对象上动态属性的检查
- 定义泛型 T、K,用于约束 userInfo 和 keys
- 为 K 增加一个泛型约束,使 K 继承 userInfo 的所有属性的联合类型, 即
K extends keyof T
function getValues<T, K extends keyof T>(userInfo: T, keys: K[]): T[K][] {
return keys.map(key => userInfo[key])
}
2
3
这样当我们指定不在对象里的属性时,就会报错
# 映射类型
TS 允许将一个类型映射成另外一个类型。
# in
介绍映射类型之前,先介绍一下 in 操作符,用来对联合类型实现遍历。
type Person = "name" | "school" | "major"
type Obj = {
[p in Person]: string
}
2
3
4
5
# Partial
Partial<T>将 T 的所有属性映射为可选的
interface IPerson {
name: string
age: number
}
let p1: IPerson = {
name: 'lin',
age: 18
}
2
3
4
5
6
7
8
9
使用了 IPerson 接口,就一定要传 name 和 age 属性
使用 Partial 改造一下,就可以变成可选属性,
interface IPerson {
name: string
age: number
}
type IPartial = Partial<IPerson>
let p1: IPartial = {}
2
3
4
5
6
7
8
Partial 原理
Partial 的实现用到了 `in` 和 `keyof`
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P]
}
2
3
4
5
6
7
8
[P in keyof T]遍历 T 上的所有属性?:设置为属性为可选的T[P]设置类型为原来的类型
# Readonly
Readonly<T>将 T 的所有属性映射为只读的,例如:
interface IPerson {
name: string
age: number
}
type IReadOnly = Readonly<IPerson>
let p1: IReadOnly = {
name: 'lin',
age: 18
}
2
3
4
5
6
7
8
9
10
11
Readonly 原理,和 Partial 几乎完全一样,
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
2
3
4
5
6
[P in keyof T]遍历 T 上的所有属性readonly设置为属性为可选的T[P]设置类型为原来的类型
# Pick
Pick 用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:
interface IPerson {
name: string
age: number
sex: string
}
type IPick = Pick<IPerson, 'name' | 'age'>
let p1: IPick = {
name: 'lin',
age: 18
}
2
3
4
5
6
7
8
9
10
11
12
13
Pick 原理
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
2
3
4
5
6
Pick 映射类型有两个参数:
- 第一个参数 T,表示要抽取的目标对象
- 第二个参数 K,具有一个约束:K 一定要来自 T 所有属性字面量的联合类型
# Record
上面三种映射类型官方称为同态,意思是只作用于 obj 属性而不会引入新的属性。
Record 是会创建新属性的非同态映射类型。
interface IPerson {
name: string
age: number
}
type IRecord = Record<string, IPerson>
let personMap: IRecord = {
person1: {
name: 'lin',
age: 18
},
person2: {
name: 'liu',
age: 25
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Record 原理
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T
}
2
3
4
5
6
- 第一个参数可以传入继承于 any 的任何值
- 第二个参数,作为新创建对象的值,被传入。
# 条件类型
T extends U ? X : Y
Exclude 和 Extract 的实现就用到了条件类型。
# Exclude
Exclude 意思是不包含,Exclude<T, U> 会返回 联合类型 T 中不包含 联合类型 U 的部分。
// b | c
type Test = Exclude<"a" | "b" | "c", "a">;
2
Exclude 原理
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T
2
3
4
- never 表示一个不存在的类型
- never 与其他类型的联合后,为其他类型
// string | number
type Test = string | number | never;
2
# Extract
Extract<T, U>提取联合类型 T 和联合类型 U 的所有交集。
type Test = Extract<"key1" | "key2", "key1">;
Extract 原理
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never
2
3
4
# 工具类型(Utility Types)
上文介绍的索引类型、映射类型和条件类型都是工具类型
# Omit
Omit<T, U>从类型 T 中剔除 U 中的所有属性。
interface IPerson {
name: string
age: number
}
type IOmit = Omit<IPerson, 'age'>
2
3
4
5
6
这样就剔除了 IPerson 上的 age 属性。
Omit 原理
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> =
Pick<T, Exclude<keyof T, K>>
2
3
4
5
Pick 用于挑选一组属性并组成一个新的类型,Omit 是剔除一些属性,留下剩余的,他们俩有点相反的感觉。
那么就可以用 Pick 和 Exclude 实现 Omit。
当然也可以不用 Pick 实现,
type Omit2<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P]
}
2
3
# NonNullable
NonNullable<T> 用来过滤类型中的 null 及 undefined 类型。
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
2
NonNullable 原理
/**
* Exclude null and undefined from T
*/
type NonNullable<T> =
T extends null | undefined ? never : T
2
3
4
5
- never 表示一个不存在的类型
- never 与其他类型的联合后,为其他类型
# Parameters
Parameters 获取函数的参数类型,将每个参数类型放在一个元组中。
type T1 = Parameters<() => string>; // []
type T2 = Parameters<(arg: string) => void>; // [string]
type T3 = Parameters<(arg1: string, arg2: number) => void>; // [arg1: string, arg2: number]
2
3
4
5
Parameters 原理
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never
2
3
4
5
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
- Parameters 首先约束参数 T 必须是个函数类型
- 判断 T 是否是函数类型,如果是则使用 infer P 暂时存一下函数的参数类型,后面的语句直接用 P 即可得到这个类型并返回,否则就返回 never
# ReturnType
ReturnType 获取函数的返回值类型。
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
2
3
ReturnType 原理
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any
2
3
4
5
懂了 Parameters,也就懂了 ReturnType,
- ReturnType 首先约束参数 T 必须是个函数类型
- 判断 T 是否是函数类型,如果是则使用 infer R 暂时存一下函数的返回值类型,后面的语句直接用 R 即可得到这个类型并返回,否则就返回 any
# TS 声明文件
# declare
当使用第三方库时,很多三方库不是用 TS 写的,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
比如,在 TS 中直接使用 Vue,就会报错
const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
2
3
4
5
6
# .d.ts
通常我们会把声明语句放到一个单独的文件(Vue.d.ts)中,这就是声明文件,以 .d.ts 为后缀。
# 使用三方库
那么当我们使用三方库的时候,是不是所有的三方库都要写一大堆 decare 的文件呢?
答案是不一定,要看社区里有没有这个三方库的 TS 类型包(一般都有)。
社区使用 @types 统一管理第三方库的声明文件,是由 DefinitelyTyped[11] 这个组织统一管理的
比如安装 lodash 的类型包,
# 自己写声明文件
比如你以前写了一个请求小模块 myFetch
function myFetch(url, method, data) {
return fetch(url, {
body: data ? JSON.stringify(data) : '',
method
}).then(res => res.json())
}
myFetch.get = (url) => {
return myFetch(url, 'GET')
}
myFetch.post = (url, data) => {
return myFetch(url, 'POST', data)
}
export default myFetch
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现在新项目用了 TS 了,要在新项目中继续用这个 myFetch,你有两种选择:
- 用 TS 重写 myFetch,新项目引重写的 myFetch
- 直接引 myFetch ,给它写声明文件
选择第二种方案
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
declare function myFetch<T = any>(url: string, method: HTTPMethod, data?: any): Promise<T>
declare namespace myFetch { // 使用 namespace 来声明对象下的属性和方法
const get: <T = any>(url: string) => Promise<T>
const post: <T = any>(url: string, data: any) => Promise<T>
}
2
3
4
5
6
7
8
比较麻烦的是需要配置才行,可以有两种选择,
- 创建一个
node_modules/@types/myFetch/index.d.ts文件,存放myFetch模块的声明文件。这种方式不需要额外的配置,但是node_modules目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。 - 创建一个
types目录,专门用来管理自己写的声明文件,将myFetch的声明文件放到types/