写在前面
这个笔记是结合极客时间梁宵老师的 Typescript 实战 和 xcatliu 的 TypeScript 入门教程联合而成。
什么是TS
TypeScript 是微软开发一款开源的编程语言,本质上是向 JavaScript 增加静态类型系统。
它是 JavaScript 的超集,它最终编译产生 JavaScript,所以可以运行在浏览器、Node.js 等等的运行时环境。
目前版本 TypeScript 3.1
为什么要用TS?
开发效率
简单来说就是 IDE 的智能感知,当一段代码有问题(比如少写了字母)时,写完马上就会有红色波浪线提示,而不是等到编译的时候才告诉你哪一行有问题
同时我们项目常用的一些第三方类库框架都有TS类型声明(@types管理),我们也可以给那些没有TS类型声明的稳定模块写声明文件,这在团队协作项目中可以提升整体的开发效率。
可维护性
- 作为一种弱类型语言,js开发一些大型/持续维护项目的时候,经常会让人体验什么是“开发一时爽,重构火葬场”。
- TS提供的 强类型系统+静态分析检查+智能感知/提示,使大规模的应用代码质量更高,运行时bug更少,更方便维护。
类型基础
强类型语言与弱类型语言
强类型:不能改变变量的数据类型,除非进行强制类型转换。代表的有 Java、Python、C/C++。
弱类型语言:能够改变变量的数据类型。常见的有 JavaScript、PHP。
静态类型语言与动态类型语言
静态类型语言:在编译阶段确定变量的类型。Java、C/C++。
动态类型语言:在执行阶段确定变量的类型。JavaScript/Python/PHP。
基本类型
ES 6 的数据类型
类型注解
相当于强类型语言的 类型声明。
const a:number = 5;
int a = 5;
十三种基本类型
通常用来表示没有任何返回值的函数
JavaScript 没有空值 (void)的概念
undefined 的 JS 中不是保留字,可以被自己定义的变量覆盖。
在 JS 中,我们可以使用 void 0 来返回 undefined
null undefined
null与 undefined是所有其它类型的一个有效值,可以赋值给别的类型
let um: undefined = undefined
let nu: null = null
let num: number = undefined;
需要把配置项 “strictNullChecks” 设为 false,才可以把 undefined 赋值给别的类型
或者使用联合类型**
let num :number | undefined | null = 2
any
未指定其类型,那么它会被识别为 any 类型
可以随意变换类型
如果不是特殊情况,不建议使用 any 类型,否则就灭必要用 TS 了。
let x
x = 1
x = []
x = () => {}
object
object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
使用object类型,就可以更好的表示像Object.create这样的API。
// 这样是错的
const obj: Object = {
x: 1,
y: 2
}
const obj: {
x: number,
y: number
} = {
x: 1,
y: 2
}
Array
1 最简单的方法是使用「类型 + 方括号」来表示数组:
let fibonacci: number[] = [1, 1, 2, 3, 5];
2 使用泛型定义数组的类型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
// 使用联合类型
let arr2: Array<number | string> = [1,'2']
1 用接口表示数组
interface NumberArray {
[index: number]: number
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5]
2 类数组
类数组(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.
实际上,不能用普通的数组来描述,而应该用接口
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
function sum() {
let args: IArguments = arguments;
}
元祖 tuple
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
元素不能多,也不能少,而且类型还要一一对应。
let xcatliu: [string, number] = ['Xcat Liu', 25];
元祖有个越界问题
**
可以添加,不能越界访问
实际开发不要这样使用。
函数
const add = (x: number, y: number) => x + y
const add9 = (x: number, y: number): number => x + y
通常,我们可以省略返回值,利用 TS 的类型推断功能。
定义函数类型
const compute: (x: number, y: number) => number
实现函数类型
compute = (a, b) => a + b
用接口定义函数的形状
interface SearchFunc {
(source: string, subString: string): boolean;
}
// 函数表达式
let mySearch: SearchFunc = function(source, subString) {
return source.search(subString) !== -1;
}
symbol
const s1: symbol = Symbol()
never
用于不会有返回值,比如抛出异常
或者死循环
// never
const error = () => {
throw new Error('error')
}
const endless = () => {
while(true) {}
}
枚举类型
枚举:一组有名字的常量集合,可以以手机里的通讯录做例子。
将程序中不容易记忆的硬编码,或者未来会变的常量,抽取处理定义为枚举类型
提供程序的可读性和可维护性。
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
数字枚举
枚举成员值默认从0递增
enum Role {
Reporter = 1,
Developer
}
// { '1': 'Reporter', '2': 'Developer', Reporter: 1, Developer: 2 }
console.log(Role.Reporter) // 1
console.log(Role.Developer) // 2
实现原理 - 反向映射
enum Role {
Reporter = 1,
Developer
}
var Role;
(function (Role) {
Role[Role["Reporter"] = 1] = "Reporter";
Role[Role["Developer"] = 2] = "Developer";
Role[Role["Maintainer"] = 3] = "Maintainer";
Role[Role["Owner"] = 4] = "Owner";
Role[Role["Guest"] = 5] = "Guest";
})(Role || (Role = {}));
字符串枚举
enum Message {
Success = 'hello'
}
var Message;
(function (Message) {
Message["Success"] = "hello";
})(Message || (Message = {}));
枚举成员
- 拥有只读属性
- const member 编译阶段被计算出结果
- 无初始值
- 对常量成员的引用
- 常量表达式
- computed member 表达式保留到程序的执行阶段
-
常量枚举
编译后被移除,不占用运行时的代码空间
- 成员只能为 const member
-
枚举/枚举成员 类型
三种情况,枚举,枚举成员都可以作为单独的类型存在
- 枚举成员没有任何初始值
- 枚举成员都是数字枚举
- 枚举成员都是字符串枚举
- 两种不同枚举成员类型的变量不能比较,编辑器会报错。
接口 interface
接口可以用来约束对象、函数以及类的解构和类型。
对象类型接口
类型检查原则
鸭式辩型法
只要满足接口的定义,多一个属性也无所谓。
不过,直接传对象字面量就不能绕过类型检查了,那么怎么绕过呢?
绕过对象字面量检查
- 将对象字面量赋值给变量
- 使用类型断言
- 为 Interface 添加字符串索引签名
- [x: string] : any
- 用任意的字符串去索引 List, 得到 any ```typescript interface List { readonly id: number; name: string;
[x: string]: any;
}
<a name="i2YSi"></a>
#### 对象的属性
**可选属性**
```typescript
interface Person {
name: string;
age?: number; // 可选属性
}
let tom: Person = {
name: 'Tom'
};
只读属性
只读属性不可以修改
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性
注意,只读的约束存在于第一次给整个对象赋值的时候,而不是第一次给只读属性赋值的时候:
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
任意属性
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
可索引类型接口
如果不确定接口的属性个数,可以使用可索引类型接口
- 数字索引 [index: number] 相当于数组
- 字符串索引 [x: string]
- 两种索引类型可以混用,不过数字索引签名的返回值必须是字符串索引签名返回值的子类型。
interface StringArray {
[index: number]: string;
}
let chars: StringArray = ['a', 'b']
interface Names {
[x: string]: string;
[y: number]: string;
}
函数类型接口
// function 定义
function add(x: number, y: number) {
return x + y
}
// 变量定义
let add:(x: number, y: number) => number;
add = (a, b) => a + b
// 接口定义 不需要指定函数的名称,直接定义参数的类型
interface F {
(arg: type): type;
}
// 类型别名 为我们函数类型起一个名字
type Add = (x: number, y: number) => number;
let add: Add = (a, b) => a + b
后三种只是函数类型的定义,而没有具体的实现。真正调用的时候,需要书写函数体。
混合类型接口
用混合类型接口定义一个类库,既有属性、方法
interface H {
(arg: type): type;
prop: type;
method(arg: type): type;
}
interface Lib {
(): void;
version: string;
doSth(): void;
}
// 实现这个接口
let lib: Lib = () => {}
lib.version = '2.0'
lib.doSth = () => {}
// 编辑器会报错,使用类型断言
let lib: Lib = (() => {}) as Lib;
类类型 接口(类实现接口)
一个接口可以约束类成员有哪些属性以及他们的类型
- 类实现接口,必须实现接口中声明的属性(公有成员)。
- 类必须实现接口中的所有属性。
- 接口只可以约束类的公有成员
- 接口也不能约束类的私有成员、受保护成员,静态成员和构造函数
interface Human {
name: string;
eat(): void;
}
class Asian implements Human {
constructor(name: string) {
this.name = name;
}
name: string
eat() {}
// 可以定义自己的属性
age: number = 0
sleep() {}
}
接口继承
接口可以像类一样相互继承,并且一个接口可以继承多个接口。
接口继承接口
- 抽离可重用的接口
- 将多个接口整合成一个接口 ```typescript interface Human { name: string; eat(): void; }
interface Man extends Human { run(): void }
// 定义一个单独的接口 interface Child { cry(): void }
// 同时继承多个接口,用逗号分开 interface Boy extends Man, Child {}
let boy: Boy = { name: ‘’, run() {}, eat() {}, cry() {} }
<a name="8ELvZ"></a>
#### 接口继承类
相当于接口把类的成员都抽象了出来。<br />只有类的成员结构,没有具体的实现。
- 只继承类的实例属性和实例方法
- 不仅抽离了 public 成员
- 而且抽离了 private 和 proteced 成员
- 抽象出类的所有成员,包括公有、私有和受保护成员。
```typescript
class Auto {
state = 1
// private state2 = 1
}
// 这个接口中就隐含了 state 属性
interface AutoInterface extends Auto {
}
// 实现 AutoInterface 接口
// 只要有私有的属性就行了
class C implements AutoInterface {
state = 1
}
// Auto 的子类也可以实现这个接口
// 不需要实现 state 属性,因为是 Auto 的子类,继承了 state 属性
class Bus extends Auto implements AutoInterface {
}
你也可以在接口中描述一个方法,在类里实现它 接口描述了类的公共部分
「接口继承类」和「接口继承接口」没有什么本质的区别。
接口和类的关系
- 接口之间可以相互继承,这样能够实现接口的复用
- 类之间也可以相互继承,可以实现方法和属性的复用
- 类可以实现接口,接口只能约束类的公有成员
- 接口可以继承类,接口可以抽离出类的成员,公有、私有和受保护成员。
- 常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用
implements
关键字来实现。这个特性大大提高了面向对象的灵活性。
interface Alarm {
alert(): void;
}
// 接口继承接口
interface LightableAlarm extends Alarm {
lightOn(): void;
lightOff(): 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');
}
}
函数
1 一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到 2 输入多余的(或者少于要求的)参数,都会报错
function sum(x: number, y: number): number {
return x + y;
}
sum(1, 2);
函数定义
定义方式
// 1 function 定义
function add(x: number, y: number) {
return x + y
}
// 2 变量定义
let add(x: number, y: number) => number;
// 3 接口定义 不需要指定函数的名称,直接定义参数的类型
interface F {
(arg: type): type;
}
// 4 类型别名 为我们函数类型起一个名字
type Add = (x: number, y: number) => number;
// 我们来实现这个函数
let add: Add = (a, b) => a + b
类型要求
- 参数类型必须声明
- 返回值类型一般无需声明,因为有类型推断
函数参数
- 参数个数
- 实参形参必须一一对应,少一个也不行、多一个也不行
- 可选参数
- 可选参数必须位于必选参数之后
- 默认参数
- 必须参数前,默认参数不可省略
- 必选参数之后的参数可以不传
- 默认参数意味着可以不传
- 必传参数意味着没有默认值
- TS 会将添加了默认值的参数识别为可选参数
- 剩余参数,和 ES6 一样的 ```javascript function add6(x: number, y = 0, z: number, q = 1) { return x + y + z + q }
function add7(x:number, …rest: number[]) { return x + rest.reduce((pre, cur) => pre + cur) }
<a name="CoX0N"></a>
### 函数重载 overload
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
在静态类型语言中,C++/Java 都有函数重载的概念,有两个函数,如果名称相同,但是参数的个数、或者类型不同,那么就实现了函数重载
好处:不需要为了相似功能的函数选择不同的函数名称,这样增强函数的可读性。
- 静态类型语言
- 函数名称相同,参数的个数或类型不同
- TS
- 预先定义一组名称相同,类型不同的函数声明,**并在一个类型最宽松的版本中实现**
- TS 中有点不一样。
- 会先尝试第一个匹配,然后如果第一个不匹配,会查下一个,所以我们要把容易匹配的写在前面。
- 前面几次都是函数定义,最后一次是函数实现。
```javascript
function add8(...rest: number[]): number;
function add8(...rest: string[]): string;
function add8(...rest: any[]) {
let first = rest[0];
if (typeof first === 'number') {
return rest.reduce((pre, cur) => pre + cur);
}
if (typeof first === 'string') {
return rest.join('');
}
}
console.log(add8(1, 2)) // 3
console.log(add8('a', 'b', 'c')) // 'abc'
类
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过 new 生成
- 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
- 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
- TS 中类和 ES 中的类有什么不同呢?
- ES6 中 引入了Class 关键字,我们终于可以像传统的 OOP 语言创建一个类了。
- TS 中类覆盖了 ES6 的类,也引入了其他的特性
- 我们可以看看有什么不同
类的基本实现
- 类中定义的属性都是实例属性
- 类中定义的方法都是原型方法
- 与 ES 不同的是,实例属性必须有初始值,或者在构造函数中被赋值,或为可选成员
class Animal {
constructor(name: string) {
this.name = name
}
name: string = 'dog'
run() {}
}
类的继承
使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。
class Cat extends Animal {
constructor(name: string, color: string) {
super(name); // 调用父类的 constructor(name)
// this 必须在 super 调用之后调用
this.color = color;
}
color: string;
sayHi() {
return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
}
}
let c = new Cat('Tom'); // Tom
console.log(c.sayHi()); // Meow, My name is Tom
类的成员修饰符
成员修饰符是对 ES 的拓展
- public
- 默认所有属性默认是 public,也可以显式的声明
- private
- 修饰的属性和方法时私有的
- 只能类中调用,不能被实例和子类中调用。
- 如果给构造函数加上 private,意思是这个类既不能被实例化,也不能被继承
- protected
- 和 private 类似,区别是它在子类中允许访问
- 受保护成员,只能在类或者子类中访问,不能在实例中访问
- 如果给构造函数加上 protected,意思是这个类既不能被实例化,只能被继承,相当于声明了一个基类
- readonly
- 只读,必须有初始值,或在构造函数中被赋值。这个属性不能被更改
- 如果多个修饰符同时存在,需要卸载其后面
- static
- 静态成员,只能通过类名来调用,不能通过实例访问,可以被子类继承。
- 静态方法,不需要实例化
- 参数属性
- 修饰符和 readonly 可以使用在构造函数参数中,等同于在类中定义该属性并且同时给该属性赋值,使代码更简洁。
abstract class Animal {
eat() {
console.log('eat')
}
abstract sleep(): void
}
class Dog extends Animal {
constructor(name: string) {
super()
this.name = name
this.pri()
}
public name: string = 'dog'
run() {}
private pri() {}
protected pro() {}
readonly legs: number = 4
static food: string = 'bones'
sleep() {
console.log('Dog sleep')
}
}
构造函数的参数也可以加上修饰符
ES 中并没有抽象类的概念,TS 中有,又是对 ES 的拓展。
不能被实例化,只能被继承(基类)
- 抽象方法包含具体实现
- 子类中就不用实现了,可以直接用。
- 抽象方法不包含具体实现
- 抽象方法的好处是知道子类中有具体实现,那么父类中就不用实现了
- abstract 关键字
- 好处
- 可以抽离出一些事物的共性
- 有利于代码的复用和扩展
- 可以实现多态
abstract class Animal {
eat() {
console.log('eat')
}
abstract sleep(): void
}
// 抽象类不能被实例化
// let animal = new Animal()
class Dog extends Animal {
constructor(name: string) {
super()
this.name = name
}
name: string = 'dog'
run() {}
sleep() {
console.log('Dog sleep')
}
}
let dog = new Dog('wangwang')
多态
在父类中我们定义个抽象方法,在多个子类中对这个方法有不同的实现。在程序运行的时候,会根据不同的对象执行不同的操作,这样就实现了运行时的绑定。
class Cat extends Animal {
sleep() {
console.log('Cat sleep')
}
}
let cat = new Cat()
let animals: Animal[] = [dog, cat]
animals.forEach(i => {
i.sleep()
})
this 类型
特殊的 TS ,this 类型。
类的成员方法可以返回一个 this,这样就可以很方便的实现链式调用。
在继承的时候,this 类型也可以表现出多态,是指 this 即可以是父类型,也可以是子类型。保持父子类之间接口调用的连贯性。
class Workflow {
step1() {
return this
}
step2() {
return this
}
}
new Workflow().step1().step2()
// 多态
class MyFlow extends Workflow {
next() {
return this
}
}
new MyFlow().next().step1().next().step2()
ES 7 中类的用法
- 实例属性
- ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义
- ES7 提案中可以直接在类里面定义
- 静态属性
- 提案中,可以使用 static 定义一个静态属性
class Animal {
name = 'Jack';
static num = 42;
constructor() {
// ...
}
}
let a = new Animal();
console.log(a.name); // Jack
console.log(Animal.num); // 42
泛型 - 重要
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
很多时候,我们希望一个函数或者一个类,支持多种数据类型。
我们有一个打印函数,希望它可以接受字符串,并且可以接受一个字符串数组,我们有几种方式来实现
- 函数重载
- 联合类型
- any 类型
- 满足需求
- 丢失类型信息,丢失了类型之间的约束关系,忽略了输入参数的类型和函数返回值类型一样 ```typescript function log(value: string): string { console.log(value) return value }
function log(value: string | string[]): string | string[] { console.log(value) return value }
function log(value: any): any { console.log(value) return value }
function log
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
> 不预先确定的数据类型,具体的类型在使用的时候才能确定。
```typescript
function log<T>(value: T): T {
console.log(value);
return value;
}
// 调用方式
log<string[]>(['a', 'b'])
// 使用类型推断
log(['a', 'b')
// 使用泛型定义一个泛型函数类型
type Log = <T>(value: T): T
let myLog: Log = log
类型 T 不需要预选的指定,保证输入参数和返回值一致。
泛型的好处
- 定义
- 调用
- 泛型函数类型
- 使用 type 关键字定义
```typescript
function log
(value: T): T { console.log(value); return value; }
- 使用 type 关键字定义
```typescript
function log
// 调用方式
log
// 使用类型别名定义一个泛型函数类型
type Log =
**定义多个类型参数**
```typescript
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
泛型接口
给你一个方法,就是把函数参数和泛型变量等同对待,泛型只是另一个维度的参数,代表类型的参数,不是代表值的参数。
// 约束接口的一个方法
interface Log {
<T>(value: T): T
}
// 用泛型约束接口的所有成员,约束整个接口
interface Log<T> {
(value: T): T
}
// 实现的时候,必须指定一个类型
let myLog: Log<number> = log
myLog(1)
// 指定一个默认类型
interface Log<T = string> {
(value: T): T
}
myLog: Log = log
myLog('1')
泛型类
- 泛型也可以约束类的成员
- 不能约束静态成员
class Log<T> {
run(value: T) {
console.log(value)
return value
}
}
let log1 = new Log<number>()
log1.run(1)
// 不指定类型,可以传入任何类型
let log2 = new Log()
log2.run({ a: 1 })
泛型参数的默认类型
在 TS 2.3 以后,我们可以为泛型中的类型参数指定默认类型。
- 当使用泛型时没有在代码中直接指定类型参数,从实际值中也无法推测出时,这个默认类型就会起作用。
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法,会报错。
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
interface Length {
length: number
}
function logAdvance<T extends Length>(value: T): T {
console.log(value, value.length);
return value;
}
// 必须传入具有 length 属性的
logAdvance([1])
logAdvance('123')
logAdvance({ length: 3 })
上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。
多个类型参数之间也可以互相约束:
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 });
要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。
类型检查机制
类型别名**
- 类型别名会给一个类型起个新名字。
- 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
类型断言
含义:用自己声明的类型覆盖类型推断
语法
(值 as 类型)
interface Foo {
bar: number
}
let foo = {} as Foo
foo.bar = 1
let bar: Foo = {
bar: 1
}
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,如果直接访问,会报错。
此时可以使用类型断言,将 something 断言成 string:
// 使用泛型
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}
// 写法2 建议
function getLength(something: string | number): number {
if ((something as string).length) {
return (something as string).length;
} else {
return something.toString().length;
}
}
- 类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的
- 类型断言只能够欺骗 TS 编译器,无法避免运行时错误。
- 类型断言可以增加我们代码的灵活性,改造旧代码会非常有效
- 要避免滥用,对上下文环境做充足的预判,没有任何根据的类型断言会给代码造成安全隐患。
- 弊端:没有按照接口的约定赋值,不会报错
- 修改:声明的时候指定类型
- 父类可以被断言为子类
- 子类既然拥有父类的属性和方法,那么被断言为父类,就不会有任何问题
- 类型声明与类型断言
- 类型声明比类型断言更加严格
- 为了增加代码的质量,最好优先使用类型声明
- 泛型与类型断言
- 也是一个解决方案
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();
类型推断 Type Inference
不需要指定变量的类型(函数的返回值类型),TS 可以根据某些规则自动地为其推断出一个类型。
let myFavoriteNumber = 'seven';
等价于
let myFavoriteNumber: string = 'seven';
- 基础类型推断
- 初始化变量
- let a = 1
- 设置函数默认参数
- let c = (x = 1) => x + 1
- 确定函数返回值
- 初始化变量
- 最佳通用类型推断
- 从多个类型中推断出一个类型,TS 尽可能推断出兼容当前所有类型的通用类型
- let b = [1, null] ,推断出 null 和 number 的联合类型
- 如果关闭 strictNullCheck,number 就能兼容 null 了
上下文推断
含义:如果 X(目标类型)= Y(源类型),则 X 兼容 Y
基本类型兼容性
- null 是 string 的子类型
let s: string = 'a'
s = null
- null 是 string 的子类型
接口兼容性
- 成员少的兼容成员多的
- 只要 Y 接口拥有 X 接口的所有属性,y 可以被赋值为 x
- 鸭式辩型法
- x 不可以赋值给 y
- 成员少的兼容成员多的
- 函数兼容性
- 参数个数:目标函数多于源函数
- 关闭 strictFunctionTypes
- 可选参数和剩余参数,遵循原则
- 固定参数兼容可选参数和剩余参数
- 可选参数不兼容固定参数和剩余参数(严格模式)
- 剩余参数可以兼容固定参数和可选参数
- 参数类型
- 基础类型,必须匹配(number、string)
- 参数为对象
- 严格模式,成员多的兼容成员少的
- 参数多的兼容参数少的,(可以把对象看成多个参数)
- 非严格模式,互相兼容
- 关闭 strictFunctionTypes
- 函数参数的双向协变
- 严格模式,成员多的兼容成员少的
- 返回值类型
- 目标函数必须与源函数相同,或为其子类型
- 成员少的兼容成员多的
- 函数重载
- 重载列表 overload
- 参数个数:目标函数多于源函数
- 枚举兼容性
- 枚举类型和数字类型相互兼容
- 枚举类型之间不兼容
- 类兼容性
- 和接口兼容性比较相似,只比较结构
- 不比较静态成员和构造函数
- 两个类具有相同的实例成员,它们的实例相互兼容
- 类中包含私有成员或受保护成员
- 如果两个类中含有私有成员,不互相兼容
- 只有父类和子类的实例,可以互相兼容 ```typescript class A { constructor(p: number, q: number) {} id: number = 1 private name: string = ‘’ } class B { static s = 1 constructor(p: number) {} id: number = 2 private name: string = ‘’ } let aa = new A(1, 2) let bb = new B(1) aa = bb bb = aa
class C extends A {} let cc = new C(1, 2) aa = cc cc = aa
- 泛型兼容性
- 泛型接口
- 只有类型参数 T 被接口成员使用时,才会影响兼容性
- 泛型函数
- 定义相同,没有指定类型参数时就兼容
```typescript
let log1 = <T>(x: T): T => {
console.log('x')
return x
}
let log2 = <U>(y: U): U => {
console.log('y')
return y
}
log1 = log2
总结一下,TS 的兼容性,允许我们在兼容的背景下相互赋值,增加了 TS 的灵活性
类型保护
含义:在特定的区块中保证变量属于某种确定的类型,可以在此区块中放心的引用此类型的属性,或者调用此类型的方法。
解决我们要使用类型断言的麻烦。
- 创建区块的办法
- instanceof
- typeof
- in
- 类型保护函数
enum Type { Strong, Week }
class Java {
helloJava() {
console.log('Hello Java')
}
java: any
}
class JavaScript {
helloJavaScript() {
console.log('Hello JavaScript')
}
javascript: any
}
// 特殊的返回值 类型谓词 lang is Java
// function isJava(lang: Java | JavaScript): lang is Java {
// return (lang as Java).helloJava !== undefined
// }
function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
// if (isJava(lang)) {
// lang.helloJava();
// } else {
// lang.helloJavaScript();
// }
// if ((lang as Java).helloJava) {
// (lang as Java).helloJava();
// } else {
// (lang as JavaScript).helloJavaScript();
// }
// instanceof
// if (lang instanceof Java) {
// lang.helloJava()
// // lang.helloJavaScript()
// } else {
// lang.helloJavaScript()
// }
// in
// if ('java' in lang) {
// lang.helloJava()
// } else {
// lang.helloJavaScript()
// }
// typeof
// if (typeof x === 'string') {
// console.log(x.length)
// } else {
// console.log(x.toFixed(2))
// }
return lang;
}
getLanguage(Type.Week, 1)
高级类型
联合类型(类型交集)
声明的类型并不确定,可以是声明中的任一个,可以增强代码的灵活性
应用场景:多类型支持
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
字面量类型
不仅限定类型,还可以限定取值的范围
let a: number | string = 1
let b: 'a' | 'b' | 'c'
let c: 1 | 2 | 3
对象的联合类型
**
只能访问所有类成员的交集
可区分的联合类型
结合联合类型和字面量类型的类型保护方法
通过两个接口的共有属性,可以创建不同的类型保护区块
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:
// 下面一句看不太懂
// 利用 never 类型
// 立即执行函数
// 检查 s 是不是 never 类型,
// 如果是 never 类型,说明前面的所有分支被覆盖了,这个分支不会走到。
// 如果不是 never 类型,说明前面的分支遗漏了
return ( (e: never) => {
throw new Error(e)
} )(s)
}
}
交叉类型(类型并集)
适合对象混入
interface DogInterface {
run(): void
}
interface CatInterface {
jump(): void
}
let pet: DogInterface & CatInterface = {
run() {},
jump() {}
}
字面量类型
- 字符串字面量
- 数字字面量
- 应用场景:限定变量取值范围
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}
索引类型
如何让 TS 发挥类型检查作用,避免取得 undefined
- 索引查询操作符 keyof T:类型 T 公共属性名的字面量联合类型
- 索引访问操作符 T[K]:对象 T 的属性 K 所代表的类型
- 泛型约束
索引类型可以实现对对象属性的查询和访问,然后再配合泛型约束,建立对象、对象属性以及属性值的约束
let obj = {
a: 1,
b: 2,
c: 3
}
// 改造前:
// function getValues(obj: any, keys: string[]) {
// return keys.map(key => obj[key])
// }
// 改造后:
// T
// K 是 T 的 keys 数组
// 返回值 T[K][]
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key])
}
// 返回 [1, 2]
console.log(getValues(obj, ['a', 'b']))
// 改造前,TS 没有标红提示我们
console.log(getValues(obj, ['d', 'e']))
// keyof T
interface Obj {
a: number;
b: string;
}
let key: keyof Obj
// T[K]
let value: Obj['a']
// T extends U
映射类型
含义:从旧类型创建出新类型
映射类型本质上是定义的泛型接口,通常还会结合索引类型,获取对象属性和属性的值,从而讲一个类型映射成我们想要的结构
interface Obj {
a: string;
b: number;
}
// 把一个接口的所有属性变成可读的
type ReadonlyObj = Readonly<Obj>
// 把一个接口的所有属性变成可选的
type PartialObj = Partial<Obj>
// 抽取 Obj 的子集
type PickObj = Pick<Obj, 'a' | 'b'>
- ReadOnly
,将 T 的所有属性变为可读的 - Partial
,将 T 的所有属性变为可选的 - Pick
,选取以 K 为属性的对象 T 的子集
以上三种类型,官方有个称呼是同态,只作用于 obj 的属性,不引入新的属性。
我们来看看非同态的,
- Record
,创建属性为 K 的新对象,属性值的类型为T
什么是非同态的类型,会创建新的属性。
// 创建一个新的类型
type RecordObj = Record<'x' | 'y', Obj>
// 等于下面这个
type RecordObj = {
x: Obj;
y: Obj;
}
官方源码
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
type Reord<K extends keyof T, T> = {
[P in K]: T;
}
条件类型
// T extends U ? X : Y
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
// 'string'
type T1 = TypeName<string>
// " object"
type T2 = TypeName<string[]>
分布式条件类型
(A | B) extends U ? X : Y
// 可以拆解成
(A extends U ? X : Y) | (B extends U ? X : Y)
type T3 = TypeName<string | string[]>
// type T3 = "string" | "object"
利用这个特性,可以帮助我们实现类型的过滤
type Diff<T, U> = T extends U ? never : T
type T4 = Diff<"a" | "b" | "c", "a" | "e">
// Diff<"a", "a" | "e"> | Diff<"b", "a" | "e"> | Diff<"c", "a" | "e">
// never | "b" | "c"
// "b" | "c"
一个应用:从类型中取出我们不需要的类型
type NotNull<T> = Diff<T, null | undefined>
type T5 = NotNull<string | number | undefined | null>
TS 中有内置的已经实现好的类型
// 和 Diff 实现一样
// 从类型 T 中过滤掉可以赋值给类型 U 的类型
// Exclude<T, U>
// NotNull 的实现
// NonNullable<T>
此外,官方还预置了一些条件类型
// Extract<T, U>
// 和
// 从类型 T 中抽取出可以赋值给类型 U 的
type T6 = Extract<"a" | "b" | "c", "a" | "e">
// type T6 = "a"
// ReturnType<T>
// 可以获取一个函数返回值的类型
// 参数是一个函数
type T8 = ReturnType<() => string>
// type T8 = string
工程篇
模块
- es6
- 导出
- 单独导出
- export let a = 1
- 批量导出
- export { b, c}
- export { b, c}
- export default
- 导出时别名
- export { g as G }
- export { g as G }
- 默认导出
- export default function() {}
- 导入再导出
- export { str as hello } from ‘.b’
- 单独导出
- 导入
- import
- 导入时起别名
- 导入所有成员,起别名
- 导入默认
- 导出
- node
- 导出
- module.exports = {}
- module.exports.a = x
- 导入
- require
- 导出
如何处理两种模块系统的兼容性
不要在模块中使用命名空间
- 实现原理:立即执行函数构成的闭包
- 早期版本,命名空间也叫内置模块,用于隔离作用域
- 随着 ES6 模块的引入,内部模块这个名称不叫了。保留的意义是兼容全局变量时代的兼容,现在在一个完全模块化的系统中,我们完全不必使用命名空间。
- 要点
- 局部变量对外不可见
- 导出成员对外可见
- 多个文件共享同名命名空间
- 依赖关系 ///
- 快捷方式
- import xxx = xxx
```typescript
///
namespace Shape { export function square(x: number) { return x * x } }
- import xxx = xxx
```typescript
///
console.log(Shape.cricle(2)) console.log(Shape.square(2))
import cricle = Shape.cricle console.log(cricle(2))
<a name="8T7Tu"></a>
### 声明合并
定义相同的两个函数,或者接口,会自动合并
> 多个具有相同名称的声明会合并为一个声明<br />
好处:
- 函数的合并
- 重载
- 接口的合并
- 合并的属性的类型必须是唯一的
- 类型不一致,会报错
- 方法的合并与函数的合并一样
- 每个函数成员成为函数重载
- 重载顺序
- 1 有一个🎁例外,函数参数为字符串字面量,提升到最顶端
- 2 不同接口之间,后面的接口靠前
- 3 接口内部,按书写顺序
- 实现的时候,一个宽泛的版本
- 类的合并
- 与接口的合并规则一致
- 命名空间
- 命名空间之间合并
- 命名空间中不可以重复定义
- 命名空间与类合并
- 在命名空间导出一个变量
- 相当于给类添加了静态属性
- 命名空间一定要放在类后面,放在前面会报错
- 命名空间与函数合并
- 同名的命名空间和同名的函数
- 等于给函数上加属性
- 命名空间一定要放在函数后面<br />
- 命名空间与枚举合并
- 命名空间导出一个函数
- 相当于给枚举类型增加了一个方法
- 位置没有要求,可以前面可以后面
```typescript
interface A {
x: number;
// y: string;
foo(bar: number): number; // 5
foo(bar: 'a'): string; // 2
}
interface A {
y: number;
foo(bar: string): string; // 3
foo(bar: string[]): string[]; // 4
foo(bar: 'b'): string; // 1
}
let a: A = {
x: 1,
y: 2,
foo(bar: any) {
return bar
}
}
class C {}
namespace C {
export let state = 1
}
console.log(C.state)
function Lib() {}
namespace Lib {
export let version = '1.0'
}
console.log(Lib.version)
enum Color {
Red,
Yellow,
Blue
}
namespace Color {
export function mix() {}
}
console.log(Color)
声明文件
如何在 TS 中引入 JS 类库,并未他们编写声明文件。
declare var jQuery: (selector: string) => any;
类库
- UMD 类库,即可以全局引入,也可以模块化引入
- 全局类库
- 模块类库
为什么要写声明文件?
当使用第三方库时,是用 JS 写的
在使用非 TS 的类库时,我们需要编写它的声明文件,才能获得对应的代码补全、接口提示等功能。
按照我的理解声明文件就是告诉TS编译器有哪些模块?有哪些变量?变量分别是什么类型?对外暴露它的 API。
有时候,有些类库的声明文件是保存在源码中。有些是单独提供的,@types/jquery。
幸运的是,绝大多数类库社区都已经编写了。
什么是声明文件
声明文件必需以 .d.ts 为后缀。
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件:
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
declare function jQuery(selector: string): any;
库的声明文件
如果该库没有提供声明文件,使用 @types 统一管理第三方库的声明文件
npm install @types/jquery —save-dev
typescript会自己去node_modules/types去找声明文件的
自己写声明文件
场景
- 使用npm包 多数有type文件 比如 vant 源码中支持TS
- 没有type文件的话 搜索@types/xxx
- 再没有的话 需要自己写
有些自己写的库没有声明文件,就得自己写了
全局库声明怎么写
declare function globalLib(options: globalLib.Options): void;
// 声明合并
// 函数和命名空间的合并
declare namespace globalLib {
const version: string;
function doSomething(): void;
// 不要放到外面,放到外面会暴露的全局
interface Options {
[key: string]: any
}
}
common.js 模块库声明怎么写
declare function moduleLib(options: Options): void
// 因为是模块化的,所以 Options 不会暴露到全局
interface Options {
[key: string]: any
}
declare namespace moduleLib {
// 其实下面的 export 不用写,用起来没啥区别
export const version: string
function doSomething(): void
}
// 这样兼容性是最好的
export = moduleLib
UMD 库声明文件
declare namespace umdLib {
const version: string
function doSomething(): void
}
// 专为 UMD 写的
// UMD 库声明全局变量
export as namespace umdLib
// 默认导出
export = umdLib
可以通过全局引入,通过 script 引入。
默认,不建议在模块中通过全局的方式调用。改一个配置项,可以关闭这个提示。
配置项:allowUmdGlobalAccess = true,就可以在模块中调用 UMD。
一些常用的
declare var
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块export as namespace
UMD 库声明全局变量declare global
扩展全局变量declare module
扩展模块/// <reference />
三斜线指令
一些例子
- export const
- export function install (vue: typeof Vue): void
- export class ActionSheet extends VanComponent {}
- export {}
一些实践
- 跟随源码,记得配置package.json types 或 typings 字段指定一个类型声明文件地址
- 发布到@types
function add8(...rest: number[]): number;
function add8(...rest: string[]): string;
function add8(...rest: any[]) {
let first = rest[0];
if (typeof first === 'number') {
return rest.reduce((pre, cur) => pre + cur);
}
if (typeof first === 'string') {
return rest.join('');
}
}
console.log(add8(1, 2))
console.log(add8('a', 'b', 'c'))
两种插件 - 给类库添加自定义的方法
- 模块插件
- 给外部类库添加自定义的方法
- 全局插件
- 给一个变量添加一些方法
/ 模块插件
import m from 'moment';
/ 扩展模块
declare module 'moment' {
// 导出自定义的方法
export function myFunction(): void;
}
m.myFunction = () => {}
/ 全局插件
/ 一般不建议这么做,给全局命名空间造成了污染。
declare global {
namespace globalLib {
function doAnyting(): void
}
}
globalLib.doAnyting = () => {}
声明文件的依赖
如果一个类很大,那么它的声明文件会很长。一般会安装模块划分,这些声明文件之间就会存在依赖。
配置文件 tsconfig.json
编译配置项,全部记住不可能,如果开发中遇到不太清楚的报错,去配置中找一找,也许一个配置项就能解决你的问题,同时,还能找到编码中不太规范的问题。
指定types路径如下
"paths": {
"*": [
"node_modules/*",
"src/types/*"
]
}
- 文件选项
- files
- 需要编译的单个文件列表
- src/a.ts
- include
- 需要编译的文件或目录
- 支持通配符
- src/* 只会编译一级目录
- src// 只会编译 src 二级目录下的文件
- src src 下所有的文件,包括一二级目录
- include 和 file 会合并
- exclude
- 需要排除的文件或目录
- 默认会排除所有的声明文件
- 默认会排除 node_modules 文件夹
- extends
- 配置文件继承
- 抽出基础配置文件
- extends: “./tsconfig.base”
- 可以覆盖 extends 引入的配置文件
- compileOnsave
- vscode 不支持这个配置
- 保存时自动编译
- files
- 编译选项
- incremental: true
- 增量编译
- 提高编译的速度
- 生成了一个 tsconfig.tsbuildinfo
- tsBuildInfoFile: “./buildFile”
- 增量编译文件的存储位置
- diagnostics: true
- 如何查看编译速度
- 打印诊断信息
- target: “es5”
- 目标语言的版本
- es5
- module: “commonjs”
- 目标模块系统
- amd
- outFile: “./app.js”
- 需要将 module 的值改为 amd
- 将多个依赖文件生成一个文件,一般会用在生成 AMD 模块中
- 需要将 module 的值改为 amd
- lib: [“dom”, “es5”, “stripthost”]
- 引用类库
- TS 需要引用的类库,即声明文件
- 如果不指定,默认会导入一些类库
- es5 引入 [“dom”, “es5”, “stripthost”]
- 比如想用数组扁平化方法
- 需要引入 [“dom”, “es5”, “stripthost”, “es2019.array”]
- allowJs: true
- 允许编译 JS 和 JSX 文件
- 通用与chckJs 一起使用
- checkJs: true
- true
- 允许在 JS 文件中报错,通常与 allosJs 一起使用
- outDir: “./out”
- 输出目录
- rootDir: “./src”
- 输入目录,用于调整输出目录结构
- 当前目录 “./“
- 就会包含 src 目录
- 就会包含 src 目录
- 指定输入文件目录
- declaration: true
- true 自动生成声明文件
- 为 index.ts 生成 index.d.ts
- declarationDir: “./d”
- 声明生成文件目录
- emitDeclarationOnly: true
- true 只生成声明文件
- sourceMap: true
- 生成 sourceMap
- inlineSourceMap: true
- 会包含在生成的 JS 文件中
- declarationMap: true
- 生成声明文件的 sourceMap
- typeRoots: []
- 声明文件目录,默认 node_modules/@types
- types: []
- 声明文件包
- 如果指定一个包,只会加载那个包的声明文件
- removeComments: true
- 删除注释
- noEmit
- 不输出文件
- noEmitOnError: true
- 发生错误时不输出文件
- noEmitHelpers: true
- 不生成 helper 函数,需额外安装 ts-helpers
- helper 副作用就是使得我们生成的文件体积变大了
- importHelpers: true
- 通过 tslib 引入 helper 函数,文件必须是模块,必须导出才是一个模块
- downlevelIteration: true
- 降级遍历器的实现 (es3/5)
- 如果是 es3 或是 es5 ,就会对遍历器有个低级的实现
- 扩展操作符是通过一个 helper 函数实现的
- strict 和类型检查相关的选型
- 如果 strict 是 true ,那么下面的选型默认都是 true
- alwaysStrict: false
- 在代码中注入 “use strict”
- noImplictAny: false
- 不允许隐式的 any 类型
- strictNulChecks: false
- 不允许吧 null、undefined 赋值给其他类型
- strictFunctionTypes
- 不允许函数参数双向协变
- strictPropertyInitialization
- 类的实例属性必须初始化
- 类的实例属性必须初始化
- strictBindCallApply
- 严格的 bind/call/apply 检查
- add.call(undefined, 1, ‘2’)
- noImplicitThis
- 不允许 this 有隐式的 any 类型
- this 有可能是 undefined
- 作用域的问题
- 下面 4 个选项和函数有关,只会报错,不会阻碍我们的编译
- noUnusedLocals: true
- 检查只声明,未使用的局部变量
- noUnusedParameters
- 检查函数中没有使用的参数
- 检查函数中没有使用的参数
- noFallthroughCaseInSwitch
- 防止 switch 语句贯穿
- switch 语句中没有 break 语句,后面的分支都会依次执行
- noImplicatRetures: true
- 保证程序的每个分支都有返回值
- if else 都要有返回值
- noUnusedLocals: true
- esModuleInterop: true
- 允许 export = 导出,既可以由 import from 导入,也可以 import = 导入
- allowUmdGlobalAccess: true
- 允许在模块中访问 UMD 全局变量
- moduleResolution: “node”
- 模块解析策略
- 默认是 “node”
- 还有一个 “classic” 解析策略
- 用于 AMD | System | ES2015
- 导入
- 相对导入
- 非相对导入
- baseUrl: “./“
- 解析非相对模块的基地址
- 默认是当前目录
- paths:
- 路径映射,相对于 baseUrl 的
- 比如我们不想导入 jquery 的默认版本,想引入 slim 版本
- paths: { “jquery”: [“node_modules/jquery/dist/jquery.slim.min.js”] }
- rootDirs: [“src”, “out”]
- 将多个目录放在一个虚拟目录下,方便运行时访问
- 这样编译器就会认为他们在同一个目录下
- 应用场景,导出的时候在同一目录下,导入的时候不在,如何保证引入路径一致
- listEmittedFiles: true
- 打印输出的文件
- listFiles: true
- 打印编译的文件,包括引用的声明文件
- incremental: true
工程引用
- TS 3.0 引入的
- 好处
- 可以灵活的配置输出目录
- 还可以使工程之间产生依赖关系
- 有利于把一个大项目拆分成几个小项目
- 优点:
- 第一,解决了输出目录的结构问题
- 第二,解决了单独构建的问题
- 第三,通过增量编译,提高了编译速度
- TS 官方的也是一个很好的工程引用的例子,
- 多个 tsconfig.json
- composite: true
- 工程可以被引用和进行增量编译
- declaration: true
- 必须开启
- 生成声明文件
- references
- 该工程所依赖的工程
- tsc —build 模式
ts-loader
- webpack 的 loader
- transpileOnly: true
- 只做语言转换,不做类型检查
- 编译速度大大加快
- 开启后,如何做类型检查呢
- 安装 fork-ts-checker-webpack-plugin
- 独立的类型检查进程
- awesome-typescript-loader
- transpileOnly
- CheckPlugin 已经内置,不需要安装
- 独立的类型检查进程
- transpileOnly
- Babel
- 只做类型转换
- @babel/preset-typescript
- @babel/proposal-calss-properties
- @babel/proposal-object-rest-spread
- tsc —watch 模式 类型检查
- 无法编译的的 TS 特性
- namespace 不要使用 babel 无法转换
类型断言,改用 as typename - const enum 常量枚举,现在不能用
- export = 默认导出,不要使用
- 只做类型转换
代码检查工具
代码检查 typeScript 可以做,为啥要用 ESLint?
TS 编译器做了两件事,编译和语法检查。
- 两者功能有一些重合
- ESLint 生成的 AST 不兼容 ES 的 AST
- 需要使用 TS 的插件(typescript-eslint) 转换成 ESTree
- 提供了解析 TS 的 parser
- 可以吧 TS AST 转化为 ESTree
- 需要使用 TS 的插件(typescript-eslint) 转换成 ESTree
- TSLint
- 官方弃用
- ESLint
- eslint
- 与 TS 的 AST 不兼容
- typescript-eslint 项目,如何和 ESLint 相结合?
- 如何让 ESLint 认得 TypeScript?
- 而且还能保持
- @typescript-esiint/parser
- 替换 ESLint 的解析器
- @typescript-eslint/eslint-plugin
- 使 ESLint 能够识别一些特殊的 TS 语法
- .eslintrc.json 怎么配置呢?
- 指定解析器 parser: ‘@typescript-eslint/parser’
- 指定插件 plugins: [‘@typescript-eslint’]
- 指定类型信息
- parserOptions: { project: ‘./tsconfig.json’ }
- 指定规则
- extends: [‘plugin:@typescript-eslint/recommended’]
- 如何让 ESLint 认得 TypeScript?
- eslint
- VSCode ESLint 插件
- eslint.autoFixOnsave
- babel-eslint 和 typescript-eslint 比较
- 适用于 Babel 体系
- 不要一起使用
单元测试
jest 是 FaceBook退出的
- ts-jest
- 有类型检查
- 有类型检查
- babel-jest
- 无类型检查
- TypeScript 工具体系
内置对象
它们的定义文件在 TypeScript 核心库的定义文件中。
ECMAScript
Boolean、Error、Date、RegExp 等。
我们可以将变量定义为这些类型
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
DOM BOM
Document、HTMLElement、Event、NodeList 等。
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
JavaScript标准库内置对象
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects
TS 核心库的定义文件
https://github.com/Microsoft/TypeScript/tree/master/src/lib
上述 ES、Dom、Bom 的定义文件都在 TS 核心库中。
- TS 核心库的定义不包含 Node.js 部分
- 要写 Node.js
- 引入第三方声明文件npm install @types/node -D
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;
}
interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
}
实战篇
React 项目实战
- 环境搭建
- ts-loader
- babel-loader
- 组件与类型
- 函数组件
- React.FC
- props 隐含 chirdren 声明
- 函数属性自动提示
- React.FC
- 类组件
- Component
- 高阶组件
- 被包装组件类型 React.ComponenentType
- 新组件类型 Component
- Hooks
- useState
- useState
- 函数组件
- 事件与类型
- event: React.FormEvent
- event: React.FormEvent
- 数据请求
- 使用 interface 维护前后端接口
- 增加可维护性
- 自动补全
- 类型检查
- 使用 interface 维护前后端接口
Redux 与类型
state
type State = Readonly<{
employeeList: EmployeeResponse
}>
action
type Action = {
type: string;
payload: any;
}
dispatch: Dispatch
// HelloClass.tsx
import React, { Component } from 'react';
import { Button } from 'antd';
interface Greeting {
name: string;
firstName?: string;
lastName?: string;
}
interface HelloState {
count: number
}
class HelloClass extends Component<Greeting, HelloState> {
state: HelloState = {
count: 0
}
static defaultProps = {
firstName: '',
lastName: ''
}
render() {
return (
<>
<p>你点击了 {this.state.count} 次</p>
<Button onClick={() => {this.setState({count: this.state.count + 1})}}>
Hello {this.props.name}
</Button>
</>
)
}
}
export default HelloClass;
// HelloHoc.tsx
import React, { Component } from 'react';
import HelloClass from './HelloClass';
interface Loading {
loading: boolean
}
function HelloHOC<P>(WrappedComponent: React.ComponentType<P>) {
return class extends Component<P & Loading> {
render() {
const { loading, ...props } = this.props;
return loading ? <div>Loading...</div> : <WrappedComponent { ...props as P } />;
}
}
}
export default HelloHOC(HelloClass);
// HelloHook.tsx
import React, { useState, useEffect } from 'react';
import { Button } from 'antd';
interface Greeting {
name: string;
firstName: string;
lastName: string;
}
const HelloHooks = (props: Greeting) => {
const [count, setCount] = useState(0);
const [text, setText] = useState<string | null>(null);
useEffect(() => {
if (count > 5) {
setText('休息一下');
}
}, [count]);
return (
<>
<p>你点击了 {count} 次 {text}</p>
<Button onClick={() => {setCount(count + 1)}}>
Hello {props.name}
</Button>
</>
)
}
HelloHooks.defaultProps = {
firstName: '',
lastName: ''
}
export default HelloHooks;
Node 项目实战
Vue 项目实战
- 环境搭建
- 手动
- 添加 vue-shim.d.ts 声明文件
- 添加 ts-loader ```typescript declare module ‘*.vue’ { import Vue from ‘vue’ export default Vue }
- 手动
```
- vue-cli
- TypeScript
- 组件封装与发布
- TypeScript
- 1 分离开发环境与生产环境入口
- 开发环境 引入 html-plugin
- 生产环境 剔除 vue 源码
- 2 SFC 封装
- 3 编写声明文件
- 4 npm publish
渐进式迁移策略
- 共存策略
- 含义:原 JS 代码不动,新增代码用 TS 编写
- 步骤:
- 添加 ts(x) 文件
- 安装 typescript
- 选择构建工具
- 保留 Babel
- 放弃 Babel
- 检查 JS
- 处理 JS 报错
- 宽松策略
- 含义:将所有的 js(x) 文件重命名为 ts(x) 文件,在不修改代码的基础上,使用最宽松的类型检查
- 步骤
- 重命名文件
- 修改 Webpack 入口
- strict: false
- 严格策略
- 含义:开启最严格的类型检查规则,处理剩余的报错
- 步骤
- strict: true
- 处理报错
思维导图
参考
https://zhuanlan.zhihu.com/p/32122243
typescript-vue-starter
TypeScript + 大型项目实战
https://ts.xcatliu.com/basics/declaration-files.html
https://www.typescriptlang.org/docs/handbook/basic-types.html
https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html
https://www.tslang.cn/docs/handbook/triple-slash-directives.html
https://zhuanlan.zhihu.com/p/51841761