前言
2020年,越来越多的前端开源项目已经或者正在使用TypeScript重构(ant design、vs code 、vue3等)。正在从事前端开发的我们,已经不能忽视TypeScript整个生态链对前端开发的影响。
首先我们看一下下面这张图片: compile time error vs runtime error。
由图片可知,编译时报错我们还能有所补救,但是当bug一旦到了线上,造成的就是系统页面的崩溃,代价可就大了。

下面这张图是stackoverflow网站2019年做的一个开发者调查报告。如图所示,TypeScript在最受开发者喜爱的语言排行榜上已经与python并列第二。
stackoverflow Developer Survey Results 2019
此外,从下面的图片可以看出在前端项目中大多bug都是类型导致的。

一个叫做 “To Type or Not to Type: Quantifying Detectable Bugs in JavaScript” 的研究表明,在选择 TypeScript 的 github 仓库中,有 15% 的 bug 得到了避免。
另一个统计调查表明:2018年,在npm包的开发者中,有62%的人在使用TS。
由此可见,TypeScript已经势不可挡。
JavaScript存在的问题
那是什么导致TS的出现呢?从根本原因来讲,JavaScript是一门动态弱类型语言,对变量的类型很宽容,容易导致一些低级错误。
比如我们常犯的手误错误(一个单词多输入了一个字符):下面的代码如果用TypeScript,IDE会自动提示标红,这样就避免把bug带到线上环境。
const regions = [
{
label: '四川省',
value: 1
},
{
label: '泸州市',
value: 20
},
{
label: '龙马潭区',
value: 3310
}
];
const region = regions.map((ele) => ele.values);
// 属性“values”在类型“{ label: string; value: number; }”上不存在。你是否指的是“value”?ts(2551)
一个知识点:编程语言弱类型与强类型的区别
- 强类型: 强制数据类型定义语言,类型安全的语言(若定义了一个整型变量a,若不进行显示转换,不能将a当作字符串类型处理)
- 弱类型:则没有约束,相对比较灵活( 一个变量可以赋不同数据类型的值)
编程语言动态语言与静态语言的区别
- 静态语言:在编译阶段确定所有变量的类型
- 动态语言: 在执行阶段确定所有变量的类型
什么是TypeScript?
来自官网的定义:TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。
TypeScript更像是一门工具,而不是一门独立的语言。
TypeScript带来了哪些好处?
- 类型安全:会在编译代码时,进行严格的类型检查。可以在编译阶段就发现大部分错误,这总比上线的时候出错好。
- 下一代JS特性
- 完善的工具链
- 提高生产力
- 增加了代码的可读性和可维护性:代码及文档
- 搭配VS code 等编辑器使用:代码提示、自动补全、自动import等功能
- 强大的社区支持
如何判断是否需要TypeScript?
- 项目的规模是怎样的
- 项目是否存在多人合作
- 是否会有新人员加入
- 项目是否需要长期维护
快速介绍:静态类型基础
TypeScript基础数据类型
ES6有如下数据类型:Boolean、Number、String、Array、Function、Object、Symbol、undefined和null。
TypeScript在ES6的基础上增加了以下数据类型:
- void: 表示没有任何返回值的类型
- never: 表示永远不会有返回值的类型(应用:函数抛出异常、死循环函数)
- 元组:元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
- 枚举
- 高级类型: 条件类型/映射类型/索引类型等
- any
- unknown
// 原始类型let bool: boolean = true
let bool2 = true
let str: string = 'echo'// str = 123
// 数组
let arr1: number[] = [1, 2, 3, 4]
let arr2: Array<number | string> = [1, 2, 3, '4']
// 元组
let tuple: [number, string] = [0, '1']
tuple.push(2)console.log(tuple)// tuple[2]
// 函数
let add = (x: number, y: number) => x + y
let compute: (x: number, y: number) => numbercompute = (a, b) => a + b
// 对象
let obj: { x: number; y: number } = { x: 1, y: 2 }
obj.x = 3
// symbol
let s1: symbol = Symbol()
let s2 = Symbol()
console.log(s1 === s2)
// undefined, null
let loading: undefined = undefined
let nu: null = null
// loading = 123
// void
let noReturn = () => {}
// any
let xx = 1
x = []一个概念:集合理论
在TypeScript中,“类型”可以理解为值的集合。类型 string 即字符串的集合。
any和unknown为一切类型的超级类型(即顶端类型),任何值都可以注解为any或者unknown,
never 是一切其它非空类型的子集合。所以never为底端类型。
TypeScript通过操作符 union(|)和 intersection(&)来识别顶端类型和底端类型,比如以下:
T | never => T // never与T取并集为T,这可以看出never就是一个空集合,任何值都不能注解为never。
T & unknown => T // unknown与T取交集为T,所以其它类型为unknown类型的子集合。never
// never
let error = () => {
throw new Error('error')
}
let endless = () => {
while(true) {}
}另一个应用是在条件类型中有运用到never: NonNullable 类型就是例子,它是将 null和 undefined 类型从 T 中排除。其定义如下:
type NonNullable<T> = T extends null | undefined ? never : T
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]unknown和any的区别
unknown更加严格,在对unknown类型执行大多数操作前(实例化、getter、函数执行)等,都要线缩小其类型范围。
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
let value2: unknown;
value2.foo.bar; // Error
value2.trim(); // Error
value2(); // Error
new value2(); // Error
value2[0][1]; // Errorunknown将类型缩小为更具体的类型, 从而实现TS的类型保护功能。
/**
* A custom type guard function that determines whether
* `value` is an array that only contains numbers.
*/
function isNumberArray(value: unknown): value is number[] {
return Array.isArray(value) && value.every((element) => typeof element === 'number');
}
const unknownValue: unknown[] = [ 15, 23, 8, 4, 42, 16 ];
const unknownValue2: unknown[] = [ '1', 23, 8, 4, 42, 16 ];
if (isNumberArray(unknownValue)) {
// Within this branch, `unknownValue` has type `number[]`,
// so we can spread the numbers as arguments to `Math.max`
const max = Math.max(...unknownValue);
console.log(max);
}
类型注解和类型推断
类型注解
相当于强类型语言中的类型声明
语法: (变量/函数): type
// 原始类型
const user: string = "Echo Zhou"
const haha: boolean = false
// 函数
function warnUser(): void {
alert("This is my warning message");
}
// 数组
let arr1: number[] = [1, 2, 3]
// 元组
let tuple: [number, string] = [0, '1']
类型推断
什么是类型推断:
不需要指定变量的类型(函数的返回值类型),TypeScript可以根据某些规则自动地为其推断出一个类型。
类型断言
类型断言是指可以手动指定一个类型,允许变量从一个类型更改为另一个类型
交叉类型与联合类型
交叉类型就是把多个类型合并成一个类型。
联合类型: 声明的类型并不确定,可以为多个类型中的一个。
interface DogInterface {
run(): void
}
interface CatInterface {
jump(): void
}
let pet: DogInterface & CatInterface = {
run() {},
jump() {}
}
let a: number | string = 1
let b: 'a' | 'b' | 'c'
let c: 1 | 2 | 3
class Dog implements DogInterface {
run() {}
eat() {}
}
class Cat implements CatInterface {
jump() {}
eat() {}
}
enum Master { Boy, Girl }
function getPet(master: Master) {
let pet = master === Master.Boy ? new Dog() : new Cat();
// pet.run()
// pet.jump()
pet.eat()
return pet
}
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case 'circle':
return Math.PI * s.radius ** 2
default:
return ((e: never) => {throw new Error(e)})(s)
}
}
console.log(area({kind: 'circle', radius: 1}))TypeScript核心
鸭子辨型法
什么是鸭子辨型法呢?如果一个对象走起路来像鸭子,嘎嘎叫起来像鸭子,那么我们就说这个对象是鸭子。
TypeScript其实就是对值所具有的结构进行类型检查,所以TS也可以被称作鸭子辨型法。
接口
为约束对象、函数、类的结构定义的一种契约(形状)
interface IWarningModProps {
form: FormProps['form'];
visible: boolean;
handleModalVisible: (flag: boolean) => void;
closeModal: () => void;
threshold: FetchCreThresholdSuccData['info']
customerId: number;
}类
成员修饰及抽象类、抽象方法
private: 只能被类的本身调用,而不能被类的实例和子类调用,构造函数加private,表明这个类不能被实例化,也不能被继承。
protected: 只能在类和子类中调用,实例中不能调用
static(类的静态成员修饰符): 只能通过类名调用
抽象类与抽象方法:用于实现多态,提取共性(父类中定义抽象方法,在子类中可以有不同的实现)。
abstract class A {
public A1: string = 'TS';
private A2: number = 123;
protected A3: string = 'GOOD';
static A4: number = 123;
public FA1() {
console.log('123');
}
private FA2() {}
protected FA3() {}
static FA4() { // 抽象方法必须被子类实现
console.log('Dog sleep');
}
abstract FA5(): void;
}
class B extends A {
public B1: string = '123';
private B2: number = 123;
protected B3: string = '1234';
static B4: number = 1234;
F5() {}
private FB5() {}
protected FB6() {}
static FB7() {
console.log('Dog sleep');
}
public FA5() {}
}
//实例化 B
let b = new B();类与接口
- 接口(Interface)主要表示抽象的行为,不能初始化属性和方法,当类实现接口时就可以具体化行为了。
- 类可以implements实现接口的时候,必须实现接口里的所有属性和方法。
- 一个接口可以继承多个接口,直接使用extend关键字,每个接口用逗号隔开即可。
- 一个类只能继承另一个类,若要实现一个类别继承多个类需要使用Mixins。
// 通过接口提取不同类之间的共性,类implements接口提高了面向对象的灵活性
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}interface和type关键字
- 首先type类型别名比起interface,除了定义对象类型以外,还可以定义交叉、联合、原始类型,相比于interface更加广泛。
- 当我们需要合并两个类型,虽然interface的extends也可以实现声明合并,但是type的交叉类型更加易读。此时建议使用type。
- 但是interface可以实现接口的extends 和 implements。interface可以实现接口的合并声明。type可以使用交叉类型代替实现。
一般情况下,当我们需要开发一个公共库的时候,最好使用interface,interface的继承与实现特性有利于使用者扩展。
interface MilkshakeProps {
name: string;
price: number;
}
interface MilkshakeMethods {
getIngredients(): string[];
}
// 接口的声明合并
interface Milkshake extends MilkshakeProps, MilkshakeMethods {}
// 相当于type的交叉类型
type Milkshake = MilkshakeProps & MilkshakeMethods;泛型
泛型无处不在,不论是第三方库的声明文件,还是我们自己的实践项目中,泛型能够保持代码的可读性、简洁性,又不失程序的灵活性。

(来自一篇文章读懂Typescript泛型及应用:juejin.cn/post/684490…)
理解泛型有两个要点:
- 不需要预先确定的数据类型,具体的类型在使用的时候才能确定。
- 泛型就像函数中的参数一样(我们可以理解为类型参数),在定义一个函数、type、interface、class 时,在名称后面加上<>表示其值也可以作为参数传递。
实践一:React类组件
在React声明文件中,对它所有api都进行了重新定义。
比如Component被定义成了一个泛型类,这个泛型类接收三个参数
- P:默认为空对象,为属性类型
- S:默认为空对象,为状态类型
- SS:不用管。
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }interface DirProps {
// ...此处省略
}
class Dir extends Component<DirProps, DirState> {
}实践二:泛型给使用 API 提供了便利
在项目中,我们通常会去扩展或者二次封装axios,比如一个实际的项目中,有一个 request.js 文件,我们定义了一个Axios工具类,提供get、post、put等公共方法,然后进行实例化导出以供使用。
请看以下例子:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { RemoveStorage } from '@/utils/utils';
import { GetStorage } from '@/utils/utils';
import { message } from 'antd';
const axiosInstance = axios.create(axiosConfig)
export interface IHttpClient {
getStoreConfig: () => {
baseURL: string;
headers: {
webToken: string;
};
};
get: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>;
post: <T>(url: string, data?: any, config?: AxiosRequestConfig) => Promise<T>;
}
...// 此处省略 ,例如添加通用配置、拦截器等。
const _http = axiosInstance;
class Axios implements IHttpClient {
public getStoreConfig() {
return {
baseURL: '/xxx/api',
headers: { webToken: GetStorage('token') }
};
}
public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const configInfo = config ? config : this.getStoreConfig();
const response: AxiosResponse = await _http.get(url, configInfo);
return response;
}
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const configInfo = config ? config : this.getStoreConfig();
const response: AxiosResponse = await _http.post(url, data, configInfo);
return response;
}
}
export const fetch: IHttpClient = new Axios();
你肯定有疑问🤔️了?
get(url: string, config?: AxiosRequestConfig ) => Promise<T> ?
如何理解Promise<T>中的泛型T?
- 你可以理解为promise变成成功态之后resolve的值,即传递给resolve的参数类型。
我们看下面这个例子:

当你没有传入泛型的时候 ,ts会推导出Promise(变成成功态以后)的返回值类型是unknown。
我们只需要作以下修改,TypeScript就可以推导出Promise返回值类型为number。

所以我们在写Api的时候就可以通过泛型T进行传递类型,然后TypeScript也可以推导出接口的返回值类型。
以下是TS自带声明文件中Promise构造函数的TS类型。
/// <reference no-default-lib="true"/>
interface PromiseConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: Promise<any>;
/**
* Creates a new Promise.
* @param executor A callback used to initialize the promise. This callback is passed two arguments:
* a resolve callback used to resolve the promise with a value or the result of another promise,
* and a reject callback used to reject the promise with a provided reason or error.
*/
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
...
/**
* Creates a new rejected promise for the provided reason.
* @param reason The reason the promise was rejected.
* @returns A new rejected Promise.
*/
reject<T = never>(reason?: any): Promise<T>;
/**
* Creates a new resolved promise for the provided value.
* @param value A promise.
* @returns A promise whose internal state matches the provided promise.
*/
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
/**
* Creates a new resolved promise .
* @returns A resolved promise.
*/
resolve(): Promise<void>;
}
declare var Promise: PromiseConstructor;
高级特性
索引类型
typeof : 获取 JS 值的类型
typeof 操作符可以用来获取一个变量声明或对象的类型。
keyof T:获取类型的键
索引查询操作符, 可以用于获取某种类型的所有键,其返回类型是联合类型。
所以这两个操作符经常联合一起使用:
const defaultColConfig = {
xs: 24,
sm: 24,
md: 12,
lg: 12,
xl: 8,
xxl: 6,
};
type colConfig = keyof typeof defaultColConfig
// type colConfig = "xs" | "sm" | "md" | "lg" | "xl" | "xxl"T[K]:获取类型的值
应用场景: 从一个对象中选取某些属性的值
interface Iobj1 {
a: number;
}
type typea = Iobj1['a']; // string
let obj1 = {
a: 1,
b: 2,
c: 3
};
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map((key) => obj[key]);
}
const a1 = getValues(obj1, [ 'a', 'b' ]); // [1,2]其它相关应用
比如在TypeScript内置类型Omit、Record等都用到了keyof。
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Record<K extends keyof any, T> = { [P in K]: T };
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>;
/* type ThreeStringProps = {
prop1: string;
prop2: string;
prop3: string;
} */映射类型
定义:从旧类型创建出新类型
// Partial: 将一个map所有属性变为可选的
type Partial<T> = { [P in keyof T]?: T[P] }
interface Obj {
a: string;
b: number;
}
type PartialObj = Partial<Obj>
// Pick: 提取map中想要的属性
type PickObj = Pick<Obj, 'a' | 'b'>
interface Customer {
id: number;
name: string;
address: string;
region: string;
parent: number;
}
type KeyProps = Pick<Customer, 'id' | 'name'>;
// Record: 构造具有类型K为type 的属性的类型T, 可用于将一个类型的属性映射到另一个类型。
type Record<K extends keyof any, T> = { [P in K]: T };
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>;
/* type ThreeStringProps = {
prop1: string;
prop2: string;
prop3: string;
} */
type RecordObj = Record<'x' | 'y', Obj>
条件类型: infer及extends关键字
extends关键字
在条件语句中,extends不是继承的意思,比如这个条件语句T extends U ? X : Y中的extends应当这样理解:如果类型T可以赋值给类型U,那么结果类型就是X,否则为Y。
infer关键字
infer :表示在 extends 条件语句中, TypeScript进行类型推断,并将推断结果存储在infer关键字后面的变量中。
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
- 上面这个条件语句中,P 表示待推断的函数返回值的类型。
实际应用场景:
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
function transCus () {
return {
name: 'echo',
region: ['四川省', '泸州市', '龙马潭区'],
address: 'xxx',
metaType: 3,
type: 3
}
}
type Customer = ReturnType<typeof transCus>
/*
type Customer = {
name: string;
region: string[];
address: string;
metaType: number;
type: number;
}
*/
条件类型还有其它很多关键字如下,具体就不一一解释了。
- Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
- Extract<T, U> -- 提取T中可以赋值给U的类型。
- NonNullable<T> -- 从T中剔除null和undefined。
- InstanceType<T> -- 获取构造函数类型的实例类型。
工程相关
声明文件
一般以d.ts为后缀的,我们称之为声明文件。
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
当我们在使用非ts编写的类库(jquery)的时候,必须为这个类库编写一个声明文件,对外暴露它的api。
有时候,一些类库的声明文件是编写在源码中(比如antd),有时候是单独提供的,需要额外的安装(比如jquery)。
大多数类库的声明文件 ,社区已为我们编写好了。我们只需要安装即可。
npm i @types/jquery -D贡献社区:definitelytyped.org/guides/cont…
总结: 所以当库本身没有自带声明文件的时候,我们需要从DefinitelTyped下载安装,如果连DefinitelTyped都没有,比如我们公司自己的第三方组件或库,那么就需要我们declare module 一下。
当我们需要给其它类库自定义方法
// 模块插件
import m from 'moment';
declare module 'moment' {
export function myFunction(): void;
}
m.myFunction = () => {}了解更多:ts.xcatliu.com/basics/decl…
配置文件tsconfig.json
files: 表示编译器需要编译的单个文件的列表。
include: [] 支持通配符,编译器需要编译的文件或者目录。
编译选项:
- target: 我们要编译成的目标语言是什么版本
'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'
- module: 要把我们的代码编译成什么模块系统
'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
{
"files": [
"src/a.ts"
],
"include": [
"src/*" // 只编译src一级目录下的文件
],
"compilerOptions": {
// "incremental": true, // 增量编译
// "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
// "diagnostics": true, // 打印诊断信息
// "target": "es5", // 目标语言的版本
// "module": "commonjs", // 生成代码的模块标准
// "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在 AMD 模块中
// "lib": [], // TS 需要引用的库,即声明文件,es5 默认 "dom", "es5", "scripthost"
// "allowJs": true, // 允许编译 JS 文件(js、jsx)
// "checkJs": true, // 允许在 JS 文件中报错,通常与 allowJS 一起使用
// "outDir": "./out", // 指定输出目录
// "rootDir": "./", // 指定输入文件目录(用于输出)
// "declaration": true, // 会为我们自动生成声明文件
// "declarationDir": "./d", // 声明文件的路径
// "emitDeclarationOnly": true, // 只生成声明文件
// "sourceMap": true, // 生成目标文件的 sourceMap
// "inlineSourceMap": true, // 生成目标文件的 inline sourceMap
// "declarationMap": true, // 生成声明文件的 sourceMap
// "typeRoots": [], // 声明文件目录,默认 node_modules/@types
// "types": [], // 声明文件包
// "removeComments": true, // 删除注释
// "noEmit": true, // 不输出文件
// "noEmitOnError": true, // 发生错误时不输出文件
// "noEmitHelpers": true, // 不生成 helper 函数,需额外安装 ts-helpers
// "importHelpers": true, // 通过 tslib 引入 helper 函数,文件必须是模块
// "downlevelIteration": true, // 降级遍历器的实现(es3/5)
// "strict": true, // 开启所有严格的类型检查
// "alwaysStrict": false, // 在代码中注入 "use strict";
// "noImplicitAny": false, // 不允许隐式的 any 类型
// "strictNullChecks": false, // 不允许把 null、undefined 赋值给其他类型变量
// "strictFunctionTypes": false // 不允许函数参数双向协变
// "strictPropertyInitialization": false, // 类的实例属性必须初始化
// "strictBindCallApply": false, // 严格的 bind/call/apply 检查
// "noImplicitThis": false, // 不允许 this 有隐式的 any 类型
// "noUnusedLocals": true, // 检查只声明,未使用的局部变量
// "noUnusedParameters": true, // 检查未使用的函数参数
// "noFallthroughCasesInSwitch": true, // 防止 switch 语句贯穿
// "noImplicitReturns": true, // 每个分支都要有返回值
// "esModuleInterop": true, // 允许 export = 导出,由import from 导入
// "allowUmdGlobalAccess": true, // 允许在模块中访问 UMD 全局变量
// "moduleResolution": "node", // 模块解析策略
// "baseUrl": "./", // 解析非相对模块的基地址
// "paths": { // 路径映射,相对于 baseUrl
// "jquery": ["node_modules/jquery/dist/jquery.slim.min.js"]
// },
// "rootDirs": ["src", "out"], // 将多个目录放在一个虚拟目录下,用于运行时
// "listEmittedFiles": true, // 打印输出的文件
// "listFiles": true, // 打印编译的文件(包括引用的声明文件)
}
}编译工具
ts-loader:
- 把typescript编译成javascript
- 配置项transpileOnly:只做语言转换,不做类型检查。
- 插件:fork-ts-checker-webapck-plugin独立的类型检查进程。
awesome-typescript-loader
与ts-loader的主要区别:
- 更适合与babel集成,使用babel转义和缓存。
- 不需要安装额外的插件,就可以把类型检查放在独立进程中进行。
为什么使用了TypeScript,还需要使用babel?
tsc和babel都具有语言转换功能,区别在于tsc具有代码检查而babel没有,但是babel有丰富的插件。
babel: 只做语言转换的插件
- @babel/preset-typescript
- @babel/proposal-class-properties
- @babel/proposal-object-rest-spread
大家可以看这篇文章:
