这是在使用过写过许多vue代码 <script lang="ts" setup> 之后,就是有在用但是没有怎么深入去学习,现在就花点时间来进入TypeScript的世界里吧

Typed JavaScript at Any Scale. 添加了类型系统的 JavaScript,适用于任何规模的项目。

# TypeScript 的特性

# 类型系统

从 TypeScript 的名字就可以看出来,「类型」是其最核心的特性

因为js太灵活了,没有什么约束,这样会导致代码质量参差不齐,维护成本高,运行错误多,而 TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。

# TypeScript 是静态类型

类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型

js是一门解释型语言,没有编译阶段,所以它是动态类型

TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 TypeScript 是静态类型

还有TS有强大的[类型推论]机制,即使不去手动声明变量 foo 的类型,也能在变量初始化时自动推论出它是一个 number 类型

let foo = 1;

let foo: number = 1;//完整的
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 编译时会报错(数字没有 split 方法),无法通过编译

# TypeScript 是弱类型

类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型

TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型,而对比之下python是弱类型

TypeScript 的核心设计理念:

在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug

# 适用于任何规模

TypeScript 非常适用于大型项目——这是显而易见的,类型系统可以为大型项目带来更高的可维护性,以及更少的 bug

TypeScript 有近百个[编译选项][],如果你认为类型检查过于严格,那么可以通过修改编译选项来降低类型检查的标准

TypeScript 还可以和 JavaScript 共存。

TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。

TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明

# 与标准同步发展

TypeScript 的另一个重要的特性就是坚持与 ECMAScript 标准同步发展

ECMAScript 是 JavaScript 核心语法的标准

普及一个新的语法从提案到变成正式标准一个新的语法从提案到变成正式标准:

  • Stage 0:展示阶段,仅仅是提出了讨论、想法,尚未正式提案。
  • Stage 1:征求意见阶段,提供抽象的 API 描述,讨论可行性,关键算法等。
  • Stage 2:草案阶段,使用正式的规范语言精确描述其语法和语义。
  • Stage 3:候选人阶段,语法的设计工作已完成,需要浏览器、Node.js 等环境支持,搜集用户的反馈。
  • Stage 4:定案阶段,已准备好将其添加到正式的 ECMAScript 标准中

如果没有安装TypeScript

npm install -g typescript

编译一个 TypeScript 文件

tsc hello.ts

约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀

给我造一个代码出来:

function sayHello(person: string) {
    if (typeof person === 'string') {
        return 'Hello, ' + person;
    } else {
        throw new Error('person is not a string');
    }
}

let user = 'Tom';
console.log(sayHello(user));

TypeScript 编译的时候即使报错了,还是会生成编译结果

# 基础

来点基础知识的干货吧

# 原始数据类型

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

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

其实也没什么的,直接看用例试试

布尔

let isDone: boolean = false;

// 编译通过
// 后面约定,未强调编译错误的代码片段,默认为编译通过

注意,使用构造函数 Boolean 创造的对象不是布尔值,而是返回一个Boolean对象

数值

使用 number 定义数值类型:

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

字符串

使用 string 定义字符串类型:

let myName: string = 'Tom';
let myAge: number = 25;

// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;

其中 [```] 用来定义 ES6 中的模板字符串,${expr}用来在模板字符串中嵌入表达式。

空值

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

Null 和 Undefined

在 TypeScript 中,可以使用 nullundefined 来定义这两个原始数据类型:

let u: undefined = undefined;
let n: null = null;

void 的区别是,undefinednull 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量

# 任意值

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

一般一个普通类型在赋值过程中是不允许改变类型的,但是如果是any类型,则被赋值为任意类型

声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值

未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

# 类型推论

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

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

# 联合类型

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

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

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

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

function getLength(something: string | number): number {
    return something.length;
}

//上例中,length 不是 string 和 number 的共有属性,所以会报错。

访问 stringnumber 的共有属性是没问题的:

function getString(something: string | number): string {
    return something.toString();
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型

# 对象的类型——接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型

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

ypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象 (opens new window)以外,也常用于对「对象的形状(Shape)」进行描述。

interface IPerson {
    name: string;
    age: number;
}

let tom: IPerson = {
    name: 'Tom',
    age: 25
};

接口一般首字母大写,有些建议接口的名称加上I前缀

定义的变量比接口少了一些属性是不允许的多一些属性也是不允许的

可见,赋值的时候,变量的形状必须和接口的形状保持一致

可选属性

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

可选属性的含义是该属性可以不存在

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};

这时仍然不允许添加未定义的属性

任意属性

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

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
} //使用 [propName: string] 定义了任意属性取 string 类型的值

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

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

只读属性

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

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527; // 报错

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

tom.id = 89757;
// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

# 数组的类型

在 TypeScript 中,数组类型有多种定义方式,比较灵活。

「类型 + 方括号」表示法

最简单的方法是使用「类型 + 方括号」来表示数组:

let fibonacci: number[] = [1, 1, 2, 3, 5];

数组的项中不允许出现其他的类型

数组泛型

我们也可以使用数组泛型(Array Generic) Array<elemType> 来表示数组:

let fibonacci: Array<number> = [1, 1, 2, 3, 5]

用接口表示数组

interface NumberArray {
    [index: number]: number;
}
// NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

类数组

类数组(Array-like Object)不是数组类型,比如 arguments

function sum() {
    let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more

上例中,arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 lengthcallee 两个属性。

# 函数的类型

函数是javaScript中的一等公民

函数声明

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

// 函数声明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

function sum(x: number, y: number): number {
    return x + y;
}

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

函数表达式

如果要我们现在写一个函数表达式(Function Expression)的定义,可能会写成这样:

let mySum = function (x: number, y: number): number {
    return x + y;
};

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

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

在 ES6 中,=> 叫做箭头函数,应用十分广泛

用接口定义函数的形状

我们也可以使用接口的方式来定义一个函数需要符合的形状:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

可选参数

前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?

与接口中的可选属性类似,我们用 ? 表示可选的参数:

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

剩余参数

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数)

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a: any[] = [];
push(a, 1, 2, 3);

重载

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

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'

# 类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型

语法

as 类型

<类型>

tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 值 as 类型

类型断言的用途

类型断言的常见用途有以下几种:

将一个联合类型断言为其中一个类型

# 声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能

声明语句

$('#foo');
// or
jQuery('#foo');

但是在 ts 中,编译器并不知道 $jQuery 是什么东西

这时,我们需要使用 declare var 来定义它的类型

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

declare var jQuery: (selector: string) => any;

jQuery('#foo');

declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除

什么是声明文件

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件

// src/jQuery.d.ts

declare var jQuery: (selector: string) => any;

声明文件必需以 .d.ts 为后缀

第三方声明文件

推荐的是使用 @types 统一管理第三方库的声明文件

@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

npm install @types/jquery --save-dev

可以在这个页面 (opens new window)搜索你需要的声明文件。

书写声明文件

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了

可不是一个简单的事情

库的使用场景主要有以下几种:

# 内置对象

内置对象是指根据标准在全局作用域(Global)上存在的对象

ECMAScript 的内置对象

BooleanErrorDateRegExp

我们可以在 TypeScript 中将变量定义为这些类型:

let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

更多的内置对象,可以查看 MDN 的文档 (opens new window)

而他们的定义文件,则在 TypeScript 核心库的定义文件 (opens new window)中。

DOM 和 BOM 的内置对象

DocumentHTMLElementEventNodeList 等。

TypeScript 中会经常用到这些类型:

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

使用ts核心库的实例

interface Math {
    /**
     * Returns the value of a base expression taken to a specified power.
     * @param x The base value of the expression.
     * @param y The exponent value of the expression.
     */
    pow(x: number, y: number): number;
}


document.addEventListener('click', function(e) {
    console.log(e.targetCurrent);
});

// index.ts(2,17): error TS2339: Property 'targetCurrent' does not exist on type 'MouseEvent'.

interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
    addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
}

Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:

npm install @types/node --save-dev

# 进阶

# 类型别名

使用 type 创建类型别名,类型别名常用于联合类型

//例子
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

# 字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个

注意,类型别名与字符串字面量类型都是使用 type 进行定义。

# 元组

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

定义一对值分别为 stringnumber 的元组:

let tom: [string, number] = ['Tom', 25];

当赋值或访问一个已知索引的元素时,会得到正确的类型:

let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

也可以只赋值其中一项:

let tom: [string, number];
tom[0] = 'Tom';

但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项

let tom: [string, number];
tom = ['Tom', 25];

越界的元素

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.

# 枚举

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

枚举使用 enum 关键字来定义:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

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

还有就是可以手动赋值

动赋值的枚举项也可以为小数或负数

常数项和计算所得项

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)

enum Color {Red, Green, Blue = "blue".length};

"blue".length 就是一个计算所得项

如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错

enum Color {Red = "red".length, Green, Blue};

// index.ts(1,33): error TS1061: Enum member must have initializer.
// index.ts(1,40): error TS1061: Enum member must have initializer.

常数枚举

常数枚举是使用 const enum 定义的枚举类型:

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];



//var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员

# 外部枚举

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

declare 定义的类型只会用于编译时的检查,编译结果中会被删除

#

传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class

TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。

# 属性和方法

使用 class 定义类,使用 constructor 定义构造函数。

通过 new 生成新实例的时候,会自动调用构造函数

下面看一个使用类的例子:

class Animal {
    name;
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        return `My name is ${this.name}`;
    }
}

let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

我们声明一个 Animal类。这个类有3个成员:一个叫做 name的属性,一个构造函数和一个 sayHi方法

我们在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。

最后一行,我们使用 new构造了 Animal类的一个实例。 它会调用之前定义的构造函数,创建一个 Animal类型的新对象,并执行构造函数初始化它。

# 类的继承

使用extends关键字实现继承,子类中使用super关键字来调用父类的构造函数和方法

class Cat extends Animal {
    constructor(name){
        super(name); // 调用父类的 constructor(name)
        console.log(this.name);
    }
    
    sayHi() {
        return 'Meow, '+ super.sayHi(); //调用父类的sayHi()
    }
}

let c = new Cat('Tom'); //Tom
console.log(c.sayHi()); // Meow, My name is Tom

# 存取器

使用 getter 和 setter 可以改变属性的赋值和读取行为:

class Animal {
    constructor(name){
        this.name = name;
    }
    get name() {
        return 'Jack';
    }
    set name(value) {
        console.log('setter: '+value);
    }
}

let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'' // setter:Tom
console.log(a.name); // Jack

# 静态方法

使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

class Animal {
    static isAnimal (a) {
        return a instanceof Animal;
    }
}

let a  = new Animal('Jack');
Animal.isAnimal(a); //true
a.isAnimal(a); // TypeError: a.isAnimal is not a function

ES7中 也有静态属性,和方法一样的用法

# Ts中类的用法

# public private 和 protected

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

# 参数属性

修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁

class Animal {
  // public name: string;
  public constructor(public name) {
    // this.name = name;
  }
}

# readonly

只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。

class Animal {
  readonly name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。

class Animal {
  // public readonly name;
  public constructor(public readonly name) {
    // this.name = name;
  }
}

# 抽象类

abstract 用于定义抽象类和其中的抽象方法。

什么是抽象类?

首先,抽象类是不允许被实例化的:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了

其次,抽象类中的抽象方法必须被子类实现:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}
let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。

下面是一个正确使用抽象类的例子:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

let cat = new Cat('Tom');

# 类的类型

给类加上 TypeScript 的类型很简单,与接口类似:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

# 泛型

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

# 简单的例子

首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++){
        result[i] = value
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

上例中,我们使用了之前提到过的数组泛型 (opens new window)来定义返回值的类型

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。

这时候,泛型就派上用场了:

function crateArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0 ;i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来

# 多个类型参数

定义泛型的时候,可以一次定义多个类型参数:

function swap<T, U>(tuple: [T,U]) :[U,T] {
    return [tuple[1],tuple[0]];
}

swap([7, 'seven']);// ['seven', 7]

例中,我们定义了一个 swap 函数,用来交换输入的元组。

# 泛型约束

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

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了

多个类型参数之间也可以互相约束:

// 我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。
function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

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

copyFields(x, { b: 10, d: 20 });

# 泛型接口

可以使用接口的方式来定义一个函数需要符合的形状,

当然也可以使用含有泛型的接口来定义函数的形状:

interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

进一步,我们可以把泛型参数提前到接口名上:

interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

注意,此时在使用泛型接口的时候,需要定义泛型的类型。

# 泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

# 在TypeScript项目中使用ESLint

ESLint是一个十分优秀的JavaScript代码检查工具,我们可以用ESLint来检查TypeScript和JavaScript代码

代码检查主要是用来发现代码错误、统一代码风格。

# 安装 ESLint

ESLint 可以安装在当前项目中或全局环境下,因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中。可以运行下面的脚本来安装:

$ npm install --save-dev eslint

由于 ESLint 默认使用 Espree (opens new window) 进行语法解析,无法识别 TypeScript 的一些语法,故我们需要安装 @typescript-eslint/parser (opens new window),替代掉默认的解析器,别忘了同时安装 typescript

$ npm install --save-dev typescript @typescript-eslint/parser

接下来需要安装对应的插件 @typescript-eslint/eslint-plugin (opens new window) 它作为 eslint 默认规则的补充,提供了一些额外的适用于 ts 语法的规则。

$ npm install --save-dev @typescript-eslint/eslint-plugin

# 创建配置文件

ESLint 需要一个配置文件来决定对哪些规则进行检查,配置文件的名称一般是 .eslintrc.js.eslintrc.json

当运行 ESLint 的时候检查一个文件的时候,它会首先尝试读取该文件的目录下的配置文件,然后再一级一级往上查找,将所找到的配置合并起来,作为当前被检查文件的配置。

我们在项目的根目录下创建一个 .eslintrc.js,内容如下:

module.exports = {
    parser: '@typescript-eslint/parser',
    plugins: ['@typescript-eslint'],
    rules: {
         // 禁止使用 var
        'no-var': "error",
        // 优先使用 interface 而不是 type
        '@typescript-eslint/consistent-type-definitions': [
            "error",
            "interface"
        ]
    }
}

以上配置中,我们指定了两个规则,其中 no-var 是 ESLint 原生的规则,@typescript-eslint/consistent-type-definitions@typescript-eslint/eslint-plugin 新增的规则

规则的取值一般是一个数组,其中第一项是 offwarnerror 中的一个,表示关闭、警告和报错。后面的项都是该规则的其他配置。

如果没有其他配置的话,则可以将规则的取值简写为数组中的第一项

关闭、警告和报错的含义如下:

  • 关闭:禁用此规则
  • 警告:代码检查时输出错误信息,但是不会影响到 exit code
  • 报错:发现错误时,不仅会输出错误信息,而且 exit code 将被设为 1(一般 exit code 不为 0 则表示执行出现错误)

# 检查一个 ts 文件

创建了配置文件之后,我们来创建一个 ts 文件看看是否能用 ESLint 去检查它。

创建一个新文件 index.ts,将以下内容复制进去:

var myName = 'Tom';

type Foo = {};

然后执行以下命令:

./node_modules/.bin/eslint index.ts

则会得到如下报错信息:

/path/to/index.ts
  1:1  error  Unexpected var, use let or const instead  no-var
  3:6  error  Use an `interface` instead of a `type`    @typescript-eslint/consistent-type-definitions

✖ 2 problems (2 errors, 0 warnings)
  2 errors and 0 warnings potentially fixable with the `--fix` option.

上面的结果显示,刚刚配置的两个规则都生效了:禁止使用 var;优先使用 interface 而不是 type

需要注意的是,我们使用的是 ./node_modules/.bin/eslint,而不是全局的 eslint 脚本,这是因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中

可是每次执行这么长一段脚本颇有不便,我们可以通过在 package.json 中添加一个 script 来创建一个 npm script 来简化这个步骤:

{
    "scripts": {
        "eslint": "eslint index.ts"
    }
}

这时只需执行 npm run eslint 即可。

# 检查整个项目的 ts 文件

我们的项目源文件一般是放在 src 目录下,所以需要将 package.json 中的 eslint 脚本改为对一个目录进行检查。由于 eslint 默认不会检查 .ts 后缀的文件,所以需要加上参数 --ext .ts

{
    "scripts": {
        "eslint": "eslint src --ext .ts"
    }
}

此时执行 npm run eslint 即会检查 src 目录下的所有 .ts 后缀的文

# 在 VSCode 中集成 ESLint 检

在编辑器中集成 ESLint 检查,可以在开发过程中就发现错误,甚至可以在保存时自动修复错误,极大的增加了开发效率。

在编辑器中集成 ESLint 检查,可以在开发过程中就发现错误,甚至可以在保存时自动修复错误,极大的增加了开发效率。

要在 VSCode 中集成 ESLint 检查,我们需要先安装 ESLint 插件,点击「扩展」按钮,搜索 ESLint,然后安装即可。

通过配置 VSCode,可以开启保存时自动修复的功能:

{
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "typescript",
            "autoFix": true
        },
    ],
    "typescript.tsdk": "node_modules/typescript/lib"
}

就可以在保存文件后,自动修复为:

let myName = 'Tom';

interface Foo {}

# 使用 Prettier 修复格式错误

ESLint 包含了一些代码格式的检查,比如空格、分号等。但前端社区中有一个更先进的工具可以用来格式化代码,那就是 Prettier (opens new window)

Prettier 聚焦于代码的格式化,通过语法分析,重新整理代码的格式,让所有人的代码都保持同样的风格。

首先需要安装 Prettier:

npm install --save-dev prettier

然后创建一个 prettier.config.js 文件,里面包含 Prettier 的配置项。Prettier 的配置项很少,这里我推荐大家一个配置规则,作为参考:

// prettier.config.js or .prettierrc.js
module.exports = {
    // 一行最多 100 字符
    printWidth: 100,
    // 使用 4 个空格缩进
    tabWidth: 4,
    // 不使用缩进符,而使用空格
    useTabs: false,
    // 行尾需要有分号
    semi: true,
    // 使用单引号
    singleQuote: true,
    // 对象的 key 仅在必要时用引号
    quoteProps: 'as-needed',
    // jsx 不使用单引号,而使用双引号
    jsxSingleQuote: false,
    // 末尾不需要逗号
    trailingComma: 'none',
    // 大括号内的首尾需要空格
    bracketSpacing: true,
    // jsx 标签的反尖括号需要换行
    jsxBracketSameLine: false,
    // 箭头函数,只有一个参数的时候,也需要括号
    arrowParens: 'always',
    // 每个文件格式化的范围是文件的全部内容
    rangeStart: 0,
    rangeEnd: Infinity,
    // 不需要写文件开头的 @prettier
    requirePragma: false,
    // 不需要自动在文件开头插入 @prettier
    insertPragma: false,
    // 使用默认的折行标准
    proseWrap: 'preserve',
    // 根据显示样式决定 html 要不要折行
    htmlWhitespaceSensitivity: 'css',
    // 换行符使用 lf
    endOfLine: 'lf'
};

接下来安装 VSCode 中的 Prettier 插件,然后修改 .vscode/settings.json

{
    "files.eol": "\n",
    "editor.tabSize": 4,
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "typescript",
            "autoFix": true
        }
    ],
    "typescript.tsdk": "node_modules/typescript/lib"
}

这样就实现了保存文件时自动格式化并且自动修复 ESLint 错误。

需要注意的是,由于 ESLint 也可以检查一些代码格式的问题,所以在和 Prettier 配合使用时,我们一般会把 ESLint 中的代码格式相关的规则禁用掉,否则就会有冲突了。

# TS推荐使用的工具!

工具/插件 用途
ESLint + Prettier 格式校验 + 自动格式化
lite-server 本地预览 TS 项目(无构建)
tsc --watch 实时编译 TypeScript
npm scripts 管理构建 / 格式化命令