1 背景
从目前 JavaScript 的发展和应用趋势来看,它的发展异常迅速,我们可以用它进行 web开发、移动应用开发、桌面软件开发、后端开发,以及未来成为趋势的 VR、WebGL及物联网的应用开发等,它的标准从2015年开始每年都会更新,即使这样,依然与 java 和 c# 这些成熟的高级语言还有很大的距离。因此使用 TypeScript 可以帮我们降低 JavaScript 弱语言的脆弱性及减少由于不正确类型导致错误产生的风险。目前包括 vue3、Antd UI 库等底层源码都有使用 typescript进行开发。除此之外,几乎所有企业的大中型项目如Vue3+VueX、React Hook等都采用typescript进行编码。
1.1 现状及解决方案
我们掌握了大部分的 TS 基本语法,却依然不能在公司的大中型项目中写出优秀的 TS 代码,主要原因还是对 TS的深度和广度掌握的都不够。因此本文将梳理 TS 的高频使用特性并结合相关实例及实践总结来帮助我们理解 TS在大型项目中面对复杂的应用场景如何正确的使用。
2 什么是类
TypeScript 类是面向对象的技术基石,包括类、属性封装、继承、多态、抽象、泛型。紧密关联的技术包括方法重写、方法重载、构造器、构造器重载、类型守卫、自定义守卫、静态方法、属性、关联引用属性等,因此全面、正确的理解类可以使我们更好的使用 TS。
类的定义:类就是拥有相同属性和方法的一系列对象的集合,类是一个模具,是从该类中包含的所有具体对象中抽象出来的一个概念。类定义了他所包含的全体对象的静态特征和动态特征。
2.1 TS 类和 ES6 类的区别
TS 类和 ES6 类 看着很像,但又有很多不同,因此区分 TS 类 和 ES6 类,既可以让我们对TS 类的优势印象更深刻,也会减少 TS 类和 ES6 类概念上的混淆。
2.1.1. 定义类属性的方式不同
(1)TS 类有多种定义属性的方式,如下:
方式1:先在类中定义属性然后在构造函数中通过 this 赋值;
方式2:构造函数直接为参数增加 public,给构造器的参数如果加上 public,这个参数就变成了一个属性;
默认构造函数会给这个属性赋值[隐式操作],示例:
class Order {
constructor(public orderId: number, public date: Date,public custname: string,
public phone: string, public orderDetailArray: Array) {
// 无需this赋值
}
......
}
(2)ES6 依然沿袭了 JavaScript 赋值的方式,在构造函数直接 this 来定义属性并赋值,代码如下:
class Order {
constructor(orderId, date, custname, phone, orderDetailArray) {
this.orderId = orderId;
this.date = date;
this.custname = custname;
this.phone = phone;
this.orderDetailArray = orderDetailArray;
}
}
2.1.2. ES6类没有访问修饰符,TS类自带访问修饰符
ES6类暂时还没有访问修饰符【public protected private】 ES6 类设置访问权限常借助call 方法或者 symbol 类型设置访问权限,其实也并没有真正彻底解决访问权限的问题。这点让ES6类在项目中对属性和方法的权限控制很受限制。 TS 类自带 public protected private 三种访问权限,可以轻松设置访问权限。
2.1.3 TS 类是静态类型语言的类,而 ES6 类按照 JavaScript 的一个语法标准设计而成
TS 是静态类型语言,具有类型注解和类型推导的能力,项目上线后隐藏语法和类型错误的的风险几乎为零,而ES6 是 JavaScript 的一个语法标准,没有数据类型检查的能力,下面举例来说明问题。
// ES6
const x = 3;
x = 10; // ES6没有任何语法错误提示
// TS
const x = 3;
x = 10;//无法分配到 "x" ,因为它是常数。
2.1.4 TS类可以生成ES5或ES6或以上版本的js文件
通过设置 tsconfig.json 文件的 target 属性值为 ES6,那么生成的js文件就是 ES6 版本的js文件
2.2 函数重载
函数重载适用于完成项目中某种相同功能但细节又不同的应用场景,常见场景为根据不同的参数类型执行不同的方法。合理的使用函数重载可以使我们的 TS 类方法更好的应用。它的意义在于让我们清晰的知道传入不同的参数得到不同的结果。
使用函数重载的优势
1 结构分明,提升代码的可读性
2 各司其职,自动提示方法和属性。每个重载签名函数完成各自功能,输出取值时不用强制转换就能出现自动提示,从而提高开发效率。
3 更利于功能扩展
//demo
function showPerson (name: string): void;
function showPerson (age: number): void;
function showPerson (play: () => void): void;
function showPerson (...args) {
console.log(args)
// 根据函数类型和数量作出不同的行为
}
函数重载总结
1、由一个实现签名+一个或多个重载签名合成,其中函数签名=函数名称+函数参数+函数参数类型+返回值类型四者合成
2、外部调用时,只能调用重载签名,不能调用实现签名
3、调用重载签名的函数时,会根据传递的参数来判断调用的是哪一个函数
4、只有实现签名具有函数体,所有的重载签名都只有签名,没有实现体
5、必须给重载签名提供返回值类型,TS 无法默认推导。
6、实现签名参数个数可以少于重载签名的参数个数,但实现签名如果准备包含重载签名的某个位置的参数 ,那实现签名就必须兼容所有重载签名该位置的参数类型【联合类型或 any 或 unknown 类型的一种】
3 类型断言、转换
3.1 TS 类型断言定义
把两种能有重叠关系的数据类型进行相互转换的一种 TS 语法,把其中的一种数据类型转换成另外一种数据类型。类型断言和类型转换产生的效果一样,但语法格式不同。
我们常见的类型断言用途有以下几种:
1、将一个联合类型断言为其中一个类型,当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name;
}
2、我们需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
上面的例子中,获取 animal.swim 的时候会报错。此时可以使用类型断言,将 animal 断言成 Fish:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
这样就可以解决访问 animal.swim 时报错的问题了。
需要注意的是,类型断言只能够掠过TypeScript 编译器,但无法避免运行时的错误。
3.2 类型断言总结如下:
1、TS 类型断言语法格式:A 数据类型的变量 as B 数据类型 。A 数据类型和 B 数据类型必须具有重叠关系
2、以下几种场景都属于重叠关系:
①. 如果 A,B 如果是类并且有继承关系
【 extends 关系】无论 A,B 谁是父类或子类, A 的对象变量可以断言成 B 类型,B 的对象变量可以断言成A类型 。但注意一般在绝大多数场景下都是把父类的对象变量断言成子类。
②. 如果 A,B 如果是类,但没有继承关系
两个类中的任意一个类的所有的 public 实例属性【不包括静态属性】加上所有的 public 实例方法和另一个类的所有 public 实例属性加上所有的 public 实例方法完全相同或是另外一个类的子集,则这两个类可以相互断言,否则这两个类就不能相互断言。
③. 如果 A 是类,B 是接口,并且 A 类实现了 B 接口【implements】,则 A 的对象变量可以断言成 B 接口类型,同样 B 接口类型的对象变量也可以断言成A类型 。
④. 如果 A 是类,B 是接口,并且 A 类没有实现了 B 接口,则断言关系和第2项的规则完全相同。
⑤. 如果 A 是类,B 是 type 定义的数据类型【就是引用数据类型,例如 Array, 对象,不能是基本数据类型,例如 string,number,boolean】,并且有 A 类实现了 B type 定义的数据类型【 implements】,则 A 的对象变量可以断言成 B type 定义的对象数据类型,同样 B type 定义的对象数据类型的对象变量也可以断言成 A 类型 。
⑥. 如果 A 是类,B 是 type 定义的数据类型,并且 A 类没有实现 B type定义的数据类型,则断言关系和第2项的规则完全相同。
⑦. 如果 A 是一个函数上参数变量的联合类型,例如 string |number,那么在函数内部可以断言成 string 或number 类型。
⑧. 多个类组成的联合类型如何断言?例如:let vechile: Car | Bus | Trunck。 vechile 可以断言成其中任意一种数据类型。 例如 vechile as Car, vechile as Bus , vechile as Trunck 。
⑨. 任何数据类型都可以转换成 any 或 unknown 类型,any 或 unknown 类型也可以转换成任何其他数据类型。
4 TS 类型守卫
4.1类型守卫定义:
在语句的块级作用域【if语句内或条目运算符表达式内】缩小变量的一种类型推断的行为。
4.2类型守卫产生时机:
TS 条件语句中遇到下列条件关键字时,会在语句的块级作用域内缩小变量的类型,这种类型推断的行为称作类型守卫 ( Type Guard )。类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型,从而减少不必要的类型断言。
- 类型判断:
typeof
- 属性或者方法判断:
in
- 实例判断:
instanceof
- 字面量相等判断:
==
,===
,!=
,!==
// 类型判断:typeof
function test(input: string | number) {
if (typeof input == 'string') {
// 这里 input 的类型「收紧」为 string
} else {
// 这里 input 的类型「收紧」为 number
}
}
//实例判断:instanceof
class Foo {}
class Bar {}
function test(input: Foo | Bar) {
if (input instanceof Foo) {
// 这里 input 的类型「收紧」为 Foo
} else {
// 这里 input 的类型「收紧」为 Bar
}
}
// 属性判断:in
interface Foo {
foo: string;
}
interface Bar {
bar: string;
}
function test(input: Foo | Bar) {
if ('foo' in input) {
// 这里 input 的类型「收紧」为 Foo
} else {
// 这里 input 的类型「收紧」为 Bar
}
}
//字面量相等判断 ==, !=, ===, !==
type Foo = 'foo' | 'bar' | 'unknown';
function test(input: Foo) {
if (input != 'unknown') {
// 这里 input 的类型「收紧」为 'foo' | 'bar'
} else {
// 这里 input 的类型「收紧」为 'unknown'
}
}
4.3 使用场景示例
## 考虑如下代码:
type Person = {
name: string;
age?: number;
};
## 获得所有age属性
function getPersonAges(persons: Person[]): number[] {
return persons
.filter((person) => person.age !== undefined)
.map((person) => person.age);
}
##但是上面的代码却会报错:
Type '(number | undefined)[]' is not assignable to type 'number[]'.
Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.
## 使用filter处理得到的结果类型仍然是Person[],到达map对Person类型的数据取值age自然会得到number | undefined类型,此时我们的数组为T[]类型,得到的结果也肯定是T[]类型的。在此种情况下,我们首先需要提供一个类型T的子类型S,然后回调函数需要提供一个 Type Guard 的断言函数,用于校验当前处理的值是否为S类型,抛弃掉不满足S类型的值,从而使得返回值的类型为S[]。使用此方式重写上面的例子:
type Person = {
name: string;
age?: number;
};
type FullPerson = Required;
function getPersonAges(persons: Person[]): number[] {
return persons
.filter(
(person): person is FullPerson => person.age !== undefined
)
.map((person) => person.age);
}
5 TS 泛型
5.1 定义
泛型是一种参数化数据类型,具有以下特点的数据类型叫泛型 :
特点一:定义时不明确使用时必须明确成某种具体数据类型的数据类型。【泛型的宽泛】
特点二:编译期间进行数据类型安全检查的数据类型。【泛型的严谨】
需要注意的点:
- 类型安全检查发生在编译期间
- 泛型是参数化的数据类型, 使用时明确化后的数据类型就是参数的值
5.2 泛型类的格式:
class 类名<泛型形参类型> 泛型形参类型一般有两种表示: 1. A-Z 任何一个字母 2. 语义化的单词来表示,绝大多数情况,泛型都是采用第一种形式表示,如下:
class ArrayList{
array: Array
add(data:T){
....
}
....
}
5.3 使用泛型类的好处
1、编译期对类上调用方法或属性时的泛型类型进行安全检查(类型安全检查),不符合泛型实际参数类型(泛型实参类型) 就编译通不过,防止不符合条件的数据增加进来。
2、一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,这无疑提高代码开发效率和减少出错率。
//示例 传入字符串时,返回一个字符串,传入数值时,返回一个数值
function xr (args: T, handler?: (result: R) => void): void{
if (handler && typeof handler === "function") {
if (typeof args === "number") {
handler(args + 1);
}
if (typeof args === "string") {
handler(`args (${ args }) (icepy)`);
}
}
}
xr("hello", (result: R) => {
console.log(result);
});
5.4 any 为什么不能替代类上的泛型
原因一:编译期间 any 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查
我们知道 any 是所有类型的父类,也是所有类型的子类。如果我们现在是一个宠物店类,希望只能添加 Dog 类,当调用 add 方法添加 Customer、Student 类必定出现编译错误,从而保证了类型安全检查,但是 any 类型无法保证类型安全检查,可以为任意类型,包括 string,number,boolean,null,undefined,never,void,unknown 基础数据类型和数组,类,接口类型, type 类型的变量全部能接受,不会进行无法进行类型安全检查。
原因二:any 类型可以获取任意数据类型的任何属性和任意方法而不会出现编译错误导致潜在错误风险,而泛型却有效的避免了此类问题发生
any 类型可以获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法,但是泛型类型被具体化成某种数据类型后,该数据类型的变量调用该数据类型之外的属性和方法时,出现编译错误,这也减少了代码隐藏潜在错误的风险。
原因三: any 类型数据获取属性和方法时无自动提示,泛型有自动提示
any 类型可以代表任意数据类型来获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法。
6 TS 交叉类型
6.1 定义:
将多个类型合并【多个类型属性和方法的并集】成的类型就是交叉类型。
对于对象类型合成的交叉类型是多个类型属性和方法的合并后的类型,属于多个类型的并集,必须是两个类型的全部属性和方法才能赋值给交叉类型变量。【可选属性和方法除外】
对于对象类型合成的联合类型变量可以接受联合类型中任意一种数据类型全部属性和方法,也可以是两个类型的全部属性和全部方法【可选属性和方法除外】,也可以是一种数据类型全部属性和方法+其他类型的某个属性和某个方法。
6.2获取属性和方法区别:
交叉类型变量可以获取两个类型的任意属性和任意方法,而联合类型的变量只能获取两个类型的共同属性和方法【交集属性和交集方法】
所以综上所述:
交叉类型的应用场景1:可应用这些没有关联的对象合并上,因为这样会极大的方便前端页面的输出。合并如同打包,比单一的一个一个的筛选输出要方便很多,整体感要好很多。
交叉类型的应用场景2: 一些 UI 库底层如果用到多个密切连接在一起的关联类型时,可以使用交叉类型来合并输出。
// 如何合并输出下面3个接口类型的对象?使用交叉类型最合适。
interface Button {
type: string
text: string
}
interface Link {
alt: string
href: string
}
interface Href {
linktype: string
target: Openlocation
}
enum Openlocation {
self = 0,
_blank,
parent
}
6.3 泛型函数+交叉类型+类型断言综合应用示例
泛型函数+ TS 交叉类型 代码:
function cross(objOne: T, objTwo: U): T & U {
let obj = {}
let combine = obj as T & U
Object.keys(objOne).forEach((key) => {
combine[key] = objOne[key]
})
Object.keys(objTwo).forEach((key) => {
if (!combine.hasOwnProperty(key)) {
combine[key] = objTwo[key]
}
})
return combine;
}
泛型函数重载+交叉类型+类型断言
function cross(objOne: T, objTwo: U): T & U
function cross
(objOne: T, objTwo: U, objThree: V): T & U & V
function cross
(objOne: T, objTwo: U, objThree?: V) {
let obj = {}
let combine = obj as T & U
Object.keys(objOne).forEach((key) => {
combine[key] = objOne[key]
})
Object.keys(objTwo).forEach((key) => {
if (!combine.hasOwnProperty(key)) {
combine[key] = objTwo[key]
}
})
if (objThree) {//如果有第三个对象传递进来实现交叉
//let obj = {}
//let combine2 = obj as T & U & V
//let combine2=combine as T & U & V
let combine2 = combine as typeof combine & V
Object.keys(objThree).forEach((key) => {
if (!combine2.hasOwnProperty(key)) {
combine2[key] = objThree[key]
}
})
return combine2// 三个对象交叉结果
}
return combine;// 两个对象交叉结果
}
7 infer 及 TS 高级类型
infer + TS 高级类型的使用总结
TypeScript 提供了较多的高级类型,通过学习高级类型可以帮助我们提高 TS 代码的灵活运用能力, 由于 TS 高级类型为我们提供了很多技巧性强的功能,当我们在项目中遇到使用这些功能的应用场景时,会给项目带来更简洁、更轻量级的实现效果。
7.1 infer 的定义:
infer 表示在 extends 条件语句中以占位符出现的用来修饰数据类型的关键字,被修饰的数据类型等到使用时才能被推断出来。
infer 占位符式的关键字出现的位置:通常infer出现在以下三个位置上。
(1)infer 出现在 extends 条件语句后的函数类型的参数类型位置上
(2)infer 出现在 extends 条件语句后的函数类型的返回值类型上
(3) infer 会出现在类型的泛型具体化类型上。
7.1.1 示例
infer 示例1:
type inferType = T extends (param: infer P) => any ? P : T
interface Customer {
custname: string
buymoney: number
}
type custFuncType = (cust: Customer) => void
type inferType = inferType// 结果为Customer
const cust: inferType = { custname: "wangwdu", buymoney: 23 }
infer 示例2:
class Subject {
constructor(public subid: number, public subname: string) {
}
}
let chineseSubject = new Subject(100, "语文")
let mathSubject = new Subject(101, "数学")
let englishSubject = new Subject(101, "英语")
let setZhangSanSubject = new Set([chineseSubject, mathSubject]);
type ss = typeof setZhangSanSubject
type ElementOf0 = T extends Set ? E : never
7.1.2 借助 infer 推断出联合类型
type ExtractAllType = T extends { x: infer U, y: infer U } ? U : T;
type T1 = ExtractAllType<{ x: string, y: number }>; ## string | number
## ExtractAllType 中 infer 格式中的属性是固定的 x 和 y,我们能够优化一下,让它能够接收任意数量:
type ExtractAllType = T extends { [k: string]: infer U } ? U : T;
type T1 = ExtractAllType<{ x: string, y: number, z: boolean }>; // string | nu
mber | boolean
## 依据这个特性,我们再看上面提取数组中的类型的功能,继续改进:
type ExtractArrayItemType = T extends (infer U)[] ? U : T;
type ItemTypes = ExtractArrayItemType<[string, number]>; // string | number
## 这里我们就实现了将元组类型转换成联合类型。
7.2 TS 高级 type 类型 - Extract
// Extract 类型定义格式 用来提取公共实例
interface ITeacher {
age: number;
gender: sex;
}
interface IStudent {
age: number;
gender: sex;
homeWork: string;
}
type CommonKeys = Extract; // "age" | "gender"
7.2.1 Extract泛型约束和类型断言对比
type func1 = (one: number, two: string) => string
type func2 = (one: number) => string
// 函数的泛型约束
// 函数类型上的泛型约束 参数类型和返回值完全相同的情况下,
// 参数少的函数类型 extends 参数多的函数类型 返回true
// 参数多的函数类型 extends 参数少的函数类型 返回false
type beginType1 = func1 extends func2 ? func1 : never// never
type beginType2 = func2 extends func1 ? func2 : never// never
type extractType1 = Extract//never
type extractType2 = Extract//= (one: number) => string
export { }
7.3 TS 高级 type 类型 - Exclude
//如果 T 中的类型在 U 不存在,则返回,否则抛弃。
type Exclude = T extends U ? never : T
demo 用Exclude来完成的获取Worker接口类型中的"age" | "email" | "salary"三个属性组成的联合类型
interface Worker {
name: string
age: number
email: string
salary: number
}
interface Student {
name: string
age: number
email: string
grade: number
}
//排除条件成立的类型,保留不符合泛型约束条件的类型
type Exclude = T extends U ? never : T
用Exclude来完成的获取Worker接口类型中的"age" | "email" | "salary" 三个属性组成的联合类型
type isResultType2 = Exclude<"age" | "email" | "salary" | "xx", keyof Worker>//xx
type isResultType22 = Exclude<"name" | "xx", keyof Worker>//xx
type isResultType23 = Exclude<"name", keyof Worker>//never
type isResultType24 = Exclude<"name" | "age" | "email" | "salary", "name">// "age" | "email" | "salary"
7.4 TS 高级 type 类型 - Record
将一个类型的所有属性值都映射到另一个类型上并创造一个新的类型。
源码及示例如下:
/**
* Construct a type with a set of properties K of type T
*/
//源码
//会将K中的所有属性值都转换为T类型,并将返回的新类型返回给proxyKType,K可以是联合类型、对象、枚举…
type Record = {
[P in K]: T;
};
//demo
import { IncomingMessage, ServerResponse } from "http";
enum Methods {
GET = "get",
POST = "post",
DELETE = "delete",
PUT = "put",
}
type IRouter = Record void>;
7.5 TS 高级 type 类型 - Pick
从 T 中,选择一组键在并集 K 中的属性。实际就是说在原有的类型 T 上 筛选出想要的全部或极个别的属性和类型
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick = {
[P in K]: T[P];
};
//示例
interface B {
id: number;
name: string;
age: number;
}
type PickB = Pick;
Pick+ Record 结合应用实践
// 理解 Pick
// 而 keyof用来获取接口的属性名【key】组成的联合类型
// K 如果 属于 keyof T 联合类型或者它的子类型
// 那么 K extends keyof T就成立
type Pick = {
// in是类型映射,=for...in 循环迭代所有的K的类型
[P in K]: T[P]
}
const todonew: Pick = {
"title": "下午3点美乐公园参加party"
}
const todonew2: Pick = {
"title": "下午3点美乐公园参加party",
"completed": false
}
interface Todo {
title: string
completed: boolean
description: string
}
type TodoPreview = Pick
const todo: TodoPreview = {
title: 'Clean room',
completed: false
}
const todo2: Pick = {
title: 'Clean room',
completed: false
}
export { }
7.6 TS 高级 type 类型 Partial+Required+ReadOnly
// Partial 一次性全部变成可选选项的type高级类型
type Partial = {
[P in keyof T]?: T[P]
}
interface ButtonProps {
type: 'button' | 'submit' | 'reset'
text: string
disabled: boolean
onClick: () => void
}
let props: Partial = {
text: "登录"
}
// Required 和Partial相反 一次性全部变成必选选项的type高级类型
type Required = {
[P in keyof T]-?: T[P]
}
// ReadOnly 一次性全部变成可读选项的type高级类型
type ReadOnly = {
readonly [P in keyof T]: T[P]
}
7.7 TS 高级 type 类型 - Omit
作用与Pick相反,Omit是排除一个字段,剩下的所有
//示例
interface C {
id: number;
name: string;
age: number;
}
type OmitC = Omit;
应用实践
type Omit = Pick>
interface Todo {
title: string
completed: boolean
description: string
// phone: number
}
type TodoPreview = Omit//type TodoPreview={}
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
export { }
8 结语
通过以上高频的 TS 特性的详解和实践总结,应该可以初步看懂包括 Vue3、Antd等在内的大型框架的源码,再加上不断地在项目中的实践和总结,我们就能更好的发挥 TS 在项目中的优势。