最近这两年,有很多人都在讨论 Typescript,无论是社区还是各种文章都能看出来,整体来说正面的信息是大于负面的,这篇文章就来整理一下我所了解的 Typescript。
TypeScript 类型
TypeScript有哪些类型
1、TypeScript 基本类型,也就是可以被直接使用的单一类型。
- 数字
- 字符串
- 布尔类型
- null
- undefined
- any
- unknown
- void
- object
- 枚举
- never
2、复合类型,包含多个单一类型的类型。
- 数组类型
- 元组类型
- 字面量类型
- 接口类型
3、如果一个类型不能满足要求怎么办?
- 可空类型,默认任何类型都可以被赋值成 null 或 undefined。
- 联合类型,不确定类型是哪个,但能提供几种选择,如:type1 | type2。
- 交叉类型,必须满足多个类型的组合,如:
type1 & type2
。
类型都在哪里使用
在变量中使用
在变量中使用时,直接在变量后面加上类型即可。
let a: number;
let b: string;
let c: null;
let d: undefined;
let e: boolean;
let obj: Ixxx = {
a: 1,
b: 2,
};
let fun: Iyyy = () => {};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在接口中使用
在接口中使用也比较简单,可以理解为组合多个单一类型。
interface IData {
name: string;
age: number;
func: (s: string) => void;
}
- 1
- 2
- 3
- 4
- 5
在函数中使用
在函数中使用类型时,主要用于处理函数参数、函数返回值。
// 函数参数
function a(all: string) {}
// 函数返回值
function a(a: string): string {}
// 可选参数
function a(a: number, b?: number) {}
- 1
- 2
- 3
- 4
- 5
- 6
TypeScript 高级用法
Typescript 中的基本用法非常简单,有 js 基础的同学很快就能上手,接下来我们分析一下 Typescript 中更高级的用法,以完成更精密的类型检查。
类中的高级用法
在类中的高级用法主要有以下几点:
- 继承
- 存储器 get set
- readonly 修饰符
- df公有,私有,受保护的修饰符
- 抽象类 abstract
继承和存储器和 ES6 里的功能是一致的,这里就不多说了,主要说一下类的修饰符和抽象类。
类中的修饰符是体现面向对象封装性的主要手段,类中的属性和方法在被不同修饰符修饰之后,就有了不同权限的划分,例如:
- public 表示在当前类、子类、实例中都能访问。
- protected 表示只能在当前类、子类中访问。
- private 表示只能在当前类访问。
class Animal {
// 公有,私有,受保护的修饰符
protected AnimalName: string;
readonly age: number;
static type: string;
private _age: number;
// 属性存储器
get age(): number {
return this._age;
}
set age(age: number) {
this._age = age;
}
run() {
("run", , );
}
constructor(theName: string) {
= theName;
}
}
= "2"; // 静态属性
const dog = new Animal("dog");
= 2; // 给 readonly 属性赋值会报错
; // 实例中访问 protected 报错
; // 正常
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
在类中的继承也十分简单,和 ES6 的语法是一样的。
class Cat extends Animal {
dump() {
();
}
}
let cat = new Cat("catname");
; // 受保护的对象,报错
; // 正常
= 2; // 正常
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在面向对象中,有一个比较重要的概念就是抽象类,抽象类用于类的抽象,可以定义一些类的公共属性、公共方法,让继承的子类去实现,也可以自己实现。
抽象类有以下两个特点:
- 抽象类不能直接实例化
- 抽象类中的抽象属性和方法,必须被子类实现
tip 经典问题:抽象类的接口的区别:
- 抽象类要被子类继承,接口要被类实现。
- 在 ts 中使用 extends 去继承一个抽象类。
- 在 ts 中使用 implements 去实现一个接口。
- 接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现。
- 抽象类是有规律的,抽离的是一个类别的公共部分,而接口只是对相同属性和方法的抽象,属性和方法可以无任何关联。
抽象类的用法如下:
abstract class Animal {
abstract makeSound(): void;
// 直接定义方法实例
move(): void {
("roaming the earch...");
}
}
class Cat extends Animal {
makeSound() {} // 必须实现的抽象方法
move() {
('move');
}
}
new Cat3();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
接口中的高级用法
接口中的高级用法主要有以下几点:
- 继承
- 可选属性
- 只读属性
- 索引类型:字符串和数字
- 函数类型接口
- 给类添加类型,构造函数类型
接口中除了可以定义常规属性之外,还可以定义可选属性、索引类型等。
interface Ia {
a: string;
b?: string; // 可选属性
readonly c: number; // 只读属性
[key: number]: string; // 索引类型
}
// 接口继承
interface Ib extends Ia {
age: number;
}
let test1: Ia = {
a: "",
c: 2,
age: 1,
};
= 2; // 报错,只读属性
const item0 = test1[0]; // 索引类型
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
接口中同时也支持定义函数类型、构造函数类型。
// 接口定义函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function (x: string, y: string) {
return false;
};
// 接口中编写类的构造函数类型检查
interface IClass {
new (hour: number, minute: number);
}
let test2: IClass = class {
constructor(x: number, y: number) {}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
函数中的高级用法
函数重载
函数重载指的是一个函数可以根据不同的入参匹配对应的类型。
例如:案例中的 doSomeThing 在传一个参数的时候被提示为 number 类型,传两个参数的话,第一个参数就必须是 string 类型。
// 函数重载
function doSomeThing(x: string, y: number): string;
function doSomeThing(x: number): string;
function doSomeThing(x): any {}
let result = doSomeThing(0);
let result1 = doSomeThing("", 2);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
This 类型
我们都知道,Javascript 中的 this 只有在运行的时候,才能够判断,所以对于 Typescript 来说是很难做静态判断的,对此 Typescript 给我们提供了手动绑定 this 类型,让我们能够在明确 this 的情况下,给到静态的类型提示。
其实在 Javascript 中的 this,就只有这五种情况:
- 对象调用,指向调用的对象
- 全局函数调用,指向 window 对象
- call apply 调用,指向绑定的对象
- 调用,指向 dom
- 箭头函数中的 this ,指向绑定时的上下文
// 全局函数调用 - window
function doSomeThing() {
return this;
}
const result2 = doSomeThing();
// 对象调用 - 对象
interface IObj {
age: number;
// 手动指定 this 类型
doSomeThing(this: IObj): IObj;
doSomeThing2(): Function;
}
const obj: IObj = {
age: 12,
doSomeThing: function () {
return this;
},
doSomeThing2: () => {
(this);
},
};
const result3 = ();
let globalDoSomeThing = ;
globalDoSomeThing(); // 这样会报错,因为我们只允许在对象中调用
// call apply 绑定对应的对象
function fn() {
(this);
}
(document)();
//
("click", function () {
(this); // body
});
- 1
- 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
- 35
- 36
- 37
泛型
泛型表示的是一个类型在定义时并不确定,需要在调用的时候才能确定的类型,主要包含以下几个知识点:
- 泛型函数
- 泛型类
- 泛型约束 T extends XXX
我们试想一下,如果一个函数,把传入的参数直接输出,我们怎么去给它编写类型?传入的参数可以是任何类型,难道我们需要把每个类型都写一遍?
- 使用函数重载,得把每个类型都写一遍,不适合。
- 泛型,用一个类型占位 T 去代替,在使用时指定对应的类型即可。
// 使用泛型
function doSomeThing<T>(param: T): T {
return param;
}
let y = doSomeThing(1);
// 泛型类
class MyClass<T> {
log(msg: T) {
return msg;
}
}
let my = new MyClass<string>();
("");
// 泛型约束,可以规定最终执行时,只能是哪些类型
function d2<T extends string | number>(param: T): T {
return param;
}
let z = d2(true);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
其实泛型本来很简单,但许多初学 Typescript 的同学觉得泛型很难,其实是因为泛型可以结合索引查询符 keyof、索引访问符 T[k] 等写出难以阅读的代码,我们来看一下。
// 以下四种方法,表达的含义是一致的,都是把对象中的某一个属性的 value 取出来,组成一个数组
function showKey1<K extends keyof T, T>(items: K[], obj: T): T[K][] {
return ((item) => obj[item]);
}
function showKey2<K extends keyof T, T>(items: K[], obj: T): Array<T[K]> {
return ((item) => obj[item]);
}
function showKey3<K extends keyof T, T>(
items: K[],
obj: { [K in keyof T]: any }
): T[K][] {
return ((item) => obj[item]);
}
function showKey4<K extends keyof T, T>(
items: K[],
obj: { [K in keyof T]: any }
): Array<T[K]> {
return ((item) => obj[item]);
}
let obj22 = showKey4<"age", { name: string; age: number }>(["age"], {
name: "yhl",
age: 12,
});
- 1
- 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
高级类型
TypeScript 中的高级类型包括:交叉类型、联合类型、字面量类型、索引类型、映射类型等,这里我们主要讨论一下
- 联合类型
- 映射类型
联合类型
联合类型是指一个对象可能是多个类型中的一个,如:let a :number | string 表示 a 要么是 number 类型,要么是 string 类型。
那么问题来了,我们怎么去确定运行时到底是什么类型?
答案是类型保护。类型保护是针对于联合类型,让我们能够通过逻辑判断,确定最终的类型,是来自联合类型中的哪个类型。
判断联合类型的方法很多:
- typeof
- instanceof
- in
- 字面量保护,=、!=、==、!=
- 自定义类型保护,通过判断是否有某个属性等
// 自定义类型保护
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
if (isFish(pet)) {
();
} else {
();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
映射类型
映射类型表示可以对某一个类型进行操作,产生出另一个符合我们要求的类型:
- ReadOnly,将 T 中的类型都变为只读。
- Partial,将 T 中的类型都变为可选。
- Exclude,从 T 中剔除可以赋值给 U 的类型。
- Extract,提取 T 中可以赋值给 U 的类型。
- NonNullable,从 T 中剔除 null 和 undefined。
- ReturnType,获取函数返回值类型。
- InstanceType,获取构造函数类型的实例类型。
我们也可以编写自定义的映射类型。
//定义toPromise映射
type ToPromise<T> = { [K in keyof T]: Promise<T[K]> };
type NumberList = [number, number];
type PromiseCoordinate = ToPromise<NumberList>;
// [Promise<number>, Promise<number>]
- 1
- 2
- 3
- 4
- 5
TypeScript 总结
TypeScript 优点
1、静态类型检查,提早发现问题。
2、类型即文档,便于理解,协作。
3、类型推导,自动补全,提升开发效率。
4、出错时,可以大概率排除类型问题,缩短 bug 解决时间。
实战中的优点:
1、发现 es 规范中弃用的方法,如:。
2、避免了一些不友好的开发代码,如:动态给 obj 添加属性。
3、vue 使用变量,如果没有在 data 定义,会直接抛出问题。
TypeScript 缺点
1、短期增加开发成本。
2、部分库还没有写 types 文件。
3、不是完全的超集。
实战中的问题:
1、还有一些坑不好解决,axios 编写了拦截器之后,typescript 反映不到 response 中去。
TypeScript学习
对于Typescript的入门学习,我自己学习时看的是阿里大佬写的Typescript学习指南,一共分为16个类目去讲解Typescript ,包括文章中讲到的内容也在这份指南中,对于想TS详细学习的或想查漏补缺的小伙伴都很适合。
篇幅原因就不列举Typescript学习指南文档内容,完整版的【直接点击获取学习】,一起进入TS的世界。
结尾
假如你工作在一个大中型项目上面,typescript 对你应该是利大于弊。可以学!还能从另外一个方便了解静态类型语言是怎么玩的,看到别人的 Java 代码居然能有看得懂的部分了。 当然要学会根据自己的需求和项目的规模合理选用工具,如果你的应用就是一个简单的展示页面,加几个 UI 状态改变,就没有必要使用。