TypeScript
TypeScript
- TypeScript
- 尚硅谷教程
TypeScrip由微软开发,是基于JavaScript的一个扩展语言TypeScript包含了JavaScript的所有内容,即:TypeScript是JavaScript的超集TypeScript增加了:静态类型检查、接口、泛型等很多现代开发特性,因此更适合大型项目的开发TypeScript需要编译为JavaScript,然后交给浏览器或其他JavaScript运行环境执行
1. 为什么需要 TypeScript
JavaScript是一种浏览器脚本语言,用于添加简单的浏览器逻辑,但现在JavaScript功能已经更复杂,甚至可用于全栈应用开发,代码量越来越多,由于当时的设计简单,开发过程中会有许多困扰,比如:- 不清楚的数据类型
- 有漏洞的逻辑无法像其他语言一样被静态检查
- 可能对不存在的属性进行访问,却没有提示
TypeScript引入了静态类型检查,可以解决上述问题,减少运行时出现异常的概率,并提高代码的可读性
2. 安装 TypeScript
- 全局安装
npm i typescript -g
2.1 编译为 .js 文件
命令行方式:
typescript包中包含typescript compile工具,可以使用tsc [filename]命令将.ts文件编译为JavaScript格式tsc app.ts # 可以省略后缀名 tsc index # ts 支持输出不完美代码,即使出错也会生成 js 文件 # 如果希望出错不更新文件,需要添加 --noEmitOnError 选项 tsc --noEmitOnError app.ts
自动化编译:
使用
tsc --init命令会生成tsconfig.json配置文件,可以配置自动编译行为使用
tsc --watch命令进入观察模式,当前文件夹下的所有ts文件发生变化时,会自动编译,当然也可以只观察指定部分文件# 创建配置文件 tsc --init # 监听所有文件变化,自动编译 tsc --watch # 监听指定文件变化,自动编译 tsc --watch app.ts index.ts
脚手架比如
webpack、vite等,都内置了TypeScript支持,可以自动生成配置文件,并完成编译,一般不需要额外处理
2.2 配置内容
- 生成出来的配置文件中包含一个
compilerOptions编译选项,此选项中包含许多的配置属性,也有被注释的属性,比如之前提到的noEmitOnError实际上是一个这里的属性 - 其中重要的配置包括
target:指定编译的目标,比如ES6、es2017、esnext(始终表示最新版本)等module:指定模块化规范,比如commonjs、amd、umd、es2015、esnext、node18、nodenext等types:指定额外的类型声明文件,是一个数组- 比如添加
@types/node之后,添加"node"就可以引入node的类型声明
- 比如添加
include:指定需要自动读取*.d.ts文件的文件夹,如果不希望递归加载,可以使用另一个files选项列举所有的需要加载的文件
2.3 注释
ts中提供了相关的特殊注释,用于对检查进行配置@ts-ignore:忽略当前行@ts-expect-error:忽略当前行,但会生成报错@ts-check:检查当前文件,用于js文件检查@ts-nocheck:关闭当前文件的所有检查,需要写在文件开头
3. 类型
ts可以通过为变量添加类型,来获得强类型语言才有的检查机制
3.1 基础类型
基础的可以为变量赋一些基本的类型,比如:
使用小写的
string、number、boolean、null、undefined、symbol为变量添加原始类型如果是对象可以使用
Object和其他的子类型,比如Array、Date、RegExp、Function等- 实际上使用
Object作为类型指的是能调用Object.prototype的方法的类型,由于自动装箱的存在,实际上仅undefined和null不行
- 实际上使用
为变量添加常量值类型,表明变量仅能接受单种值
如果为函数参数添加类型,不按类型声明提供值,会在编译时报错,包括函数参数个数不正确也会报错
可以为一个函数添加多种类型的签名,最终实现时要求能对所有类型的声明做处理,当然这样函数体还是不能重载的,如果希望更好地实现类似重载的效果可以使用泛型
如果使用的变量名未定义,会报错,假设未定义变量
nme有类似的命名name,甚至会指出可能是拼写错误,是否指的是namelet name: string = 'Tom' let age: number = 18 let flag: boolean = true let list: number[] = [1, 2, 3] let time: Date = new Date() let title: 'Tom' = 'Tom' function sum(x: number, y: number): number { return x + y } function add(a:number, b:number):string function add(a:string, b:string):string function add(a: number | string, b: number | string): string { return a + "" + b }基本类型与包装类型
- 在声明类型时,比如字符串有两种类型可以对应声明,也就是
string和String string是原始类型,String是包装类型,看起来好像在使用一般的字符串时,两种类型没什么区别,但实际开发中ts建议使用string作为类型声明String类型比起string能存储String对象,看起来更灵活,但包装对象一般不显式声明且占内存更多- 对于
String类型还好,如果是Number类型在取值时需要使用valueOf()方法获取原始值,很复杂 - 一般使用
string这样的原始类型就够了,String带来的灵活似乎没有足够的意义
- 在声明类型时,比如字符串有两种类型可以对应声明,也就是
类型推断:
ts会根据赋值的类型自动推导出变量的类型,这样可以方便地识别类型,但一般情况下还是建议显式指定类型能够正常推断赋值后的变量类型
使用
typeof和instanceof时,虽然得到的结果和js相同,但会进行类型收窄,之后会缩小类型范围// 实际上能推断出是字符串 let name = 'Tom' console.log(name.length)
类型断言:
ts提供了类型断言,用于告诉编译器变量的类型,避免编译器判断错误as关键字:用于将变量转换为指定类型,也可用于设置值为固定常量as const<>语法:类似as可将变量转换为指定类型,但由于在jsx中<>有其他含义,不经常使用satisfies:用于校验变量类型,相比as,satisfies更安全,会检查属性缺失情况,且保留原始的类型和补全
is关键字:用于判断变量是否为指定类型,用于创建类型判断函数变量后的
!:非空断言,告诉编译器,此变量不为null或undefined- 在类中的属性声明中包含
!,表示此属性不在构造函数中初始化,但一定会被初始化
- 在类中的属性声明中包含
变量后的
?:用于安全访问变量属性,如果变量是undefined,则返回undefined,不访问对应属性typeof:从对象获取它的类型,和js关键字用途不同keyof:用于获取类型的所有键,构建联合类型,比如对const user = { name: "Tom", age: 18 };操作,keyof typeof user得到'name' | 'age'typeof user[keyof typeof user]有奇效,可以获取到所有的值构成的联合类型,可用来构建as const枚举
// as 转换为 HTMLElement 类型 const el = document.getElementById("app") as HTMLElement; // <> 转换为 HTMLElement 类型 const el = <HTMLElement>document.getElementById("app"); // ! 确保 el 不为 null 或 undefined el!.style.display = "none"; // ? 访问 el 的属性,如果 el 为 null 或 undefined,则返回 undefined const text = el?.textContent; // is 用于创建类型判断函数 function isString(value: any): value is string { return typeof value === "string"; } // typeof 用于获取对象类型 const user = { name: "Tom", age: 18 }; const type = typeof user; // keyof 用于获取联合类型 const keys = keyof typeof user; // keys = 'name' | 'age' const values = typeof user[keyof typeof user]; // values = string | number
3.2 常用对象类型的声明
- 简单的类型声明实际上仅使用这部分内容,如果需要更复杂的声明,请考虑使用接口、泛型、额外约束等
3.2.1 一般对象
- 对于一般对象,可以使用类似一个真实对象的形式进行声明,键就是真实的会出现的键,值是类型
多个键声明之间可以使用
,进行分隔,也可以使用;分隔,甚至使用换行分隔声明的键在最终的变量中必须出现,如果希望键可以不存在,应该在键名后加上
?如果希望创建有额外的动态键和类型的对象,可以使用
{ [key: type]: type }进行声明其他的不匹配的键都将作为此类型,其中的key实际上可以换成任意名称let a: { name: string, age: number } = {name: 'Tom', age: 18} let b: { name?: string; age?: number } = {} let c: { name: string age: number } = {name: 'Tom', age: 18} let c: { name?: string, age?: number, [key: string]: string } = {name: 'Tom', aaa: '123'}
3.2.2 数组对象
对于数组,
ts要求限制数组中的元素类型,实际上有两种声明方法- 使用
type[]进行声明 - 使用
Array<type>进行声明
之前有提到过原始类型和包装类型之间的类型选择,但这个问题在
Array中不存在,也没有array类型let list1: number[] = [1, 2, 3] let list2: Array<number> = [1, 2, 3] // 更灵活的类型,ts 是支持自定义类型的,见 type 声明 let list: (number | string)[] = [1, '2', 3]- 使用
3.2.3 函数对象
对于函数,可以为函数变量设置约束类型,确保提供的函数与预想一致
写法类似箭头函数,但不需要写具体内容,其中的参数名称不用和之后一致,只要保证参数列表中每一个变量的类型对应
函数参数是逆可变的,意思是最终的赋值的函数参数个数应该不能超过声明的个数,但可以少于声明的个数
函数的返回值可以大于声明的范围,但至少包含声明的返回值类型
函数重载:
js是不支持重载的,不过ts中允许为同一个函数多次声明,然后要求函数需要满足多个声明的签名- 返回值将为多次声明的合并
- 参数列表中应该考虑每个声明中该位置的参数类型
- 函数实现必须跟在所有声明之后
- 必须使用
function声明函数,不能使用变量声明,这样才能进行多次变量类型声明
let fn: (a:number, b:number) => number fn = function (x, y) { return x + y } fn = function (x) { return x } // 函数重载 function fn1(a:number, b?:number): number function fn1(a:string, b:string): 0|1 function fn1(a:number|string, b?:string|number): number | string { if (typeof a === 'number' && typeof b === 'number' || typeof b === 'undefined') { return a + (b || 0) } else if (typeof a === 'string' && typeof b === 'string') { return a === b? 0 : 1 } return "a + b" }this约束在
typescript中可以在参数列表中添加this参数,表示函数的上下文环境- 这个参数在编译为
js时,会忽略掉 - 可以避免不合理的上下文绑定
function fn(this: string, a: number, b: number): number { return a + b + this.length } fn.apply('Tom',[1, 2]) // 可行 const obj = {name: 'Tom', fn} obj.fn(1, 2) // 报错
其他限制:虽然使用这种方法限制函数类型似乎非常方便,但实际上有的时候还需要更灵活的限制
不允许使用一般函数,需要能初始化变量的
class声明:在函数类型前添加new如果我希望可变参数个数:使用类似解构的声明方法
...args要求类有其他的静态属性:使用
{}包裹一个匿名的构造函数和其他静态属性// new 表示接收的函数必须可构造 // ...args 表示可变参数个数 // args: any[] 表示参数值任意 // {} 表示返回对象类型,非 null 和 undefined let fn: (new (...args: any[]) => {}) fn = class {} // class 构造函数要有静态属性 function getConstructor(c: ({ // 构造函数参数可少于1个 new (name: string): {} // 这个属性本来类就应该有,因此不会提示错误 name: string a: number })) {} class A {} // 仅报错:没有 static a 属性 getConstructor(A)
3.3 常用的额外类型
ts中添加了其他的类型anyunknownvoidnevertupleenumobject
3.3.1 any
any类型表示任意类型,使用any类型时,ts会忽略类型检查any类型可以存储任意类型的值实际上不传参数时定义变量,比如
let b得到的就是any类型any类型可以赋值给任意类型any类型的任意属性访问都不检查,在访问不存在的属性时,不报错let a: any = 123 // any类型可以接收任意类型 a = 'Tom' a = true // 隐式 any let b b = 123 b = 'Tom' // 可以赋值给任意类型 let c: string = a
3.3.2 unknown
unknown类型表示未知类型,使用unknown类型时,ts会进行类型检查,是类型安全的any,与any不同的是unknown类型要求使用前进行类型判断赋值给其他变量前需要进行判断或转换
在访问属性时要求进行前置类型判断或转换
let a: unknown = 123 let b: string // 类型判断 if (typeof a === 'string') { b = a } // 类型转换 b = a as string b = <string>a
3.3.3 never
never类型表示永远不会出现的类型,一般不使用,表示不能有值将
never作为类型提供给函数返回值,此函数只有永远执行不完才能通过类型检查,一般可以说函数满足以下条件之一:- 函数执行过程中必须
throw new Error('错误') - 函数必须是死循环
- 函数执行过程中必须
将
never作为变量的类型,变量初始化之后不能进行任何赋值操作,一般没有意义一般是
ts的类型推断得到的类型,比如永远无法到达的分支中的变量类型// 函数返回 never function error(message: string): never { throw new Error(message) } function infiniteLoop(): never { // while (true) {} infiniteLoop() } // 自动推断得到的 never let a: string = "1" if (typeof a === "string") { console.log(a.charAt(0)) } else { // 此时 a 的类型被推断为 never console.log(a) }
3.3.4 void
void类型表示函数没有返回值,或返回值类型为undefined,意味着函数不会返回任何值,并且调用者不应该读取返回值进行操作js默认函数在不写返回值时会返回undefined,但如果使用undefined作为返回值,无法限制读取返回值进行操作void类型虽然也仅接受返回undefined,但与undefined的区别在于,void返回值不能被用于之后的操作(虽然如果是作为any的话还是可能被使用的,但是值是undefined)function print(): void { console.log('打印') } let res = print() // 此行报错,无法测试 void 返回值的真实性 if (res) { // 被推断为 never 类型的 res console.log(res) } // 此行不会报错,因为 log 接受 any 类型 console.log(res)
3.3.5 object
object类型表示任意非原始类型对象相比
Object类型,能存储除了undefined和null(唯二无法调用Object.prototype方法的类型)外的所有变量类型,object类型范围更小,但也很大,一般不使用let a: object = {} a = [1, 2, 3] a = function () {} // 报错 a = 123 a = "Tom" a = undefined a = null // ------------------ let b: Object = {} b = [1, 2, 3] b = function () {} b = 123 b = "Tom" // 报错 b = null b = undefined
3.3.6 tuple
tuple类型表示一个元组,元组类型定义一个数组,数组的元素类型和数量是固定的且内容符合类型预测的如果存储的元素超过声明的类型数量,也就是最后的元素没有对应的类型会报错
可以在末尾使用
?作为可选元素每个元素的类型是已知且可不同的
可以使用
...number[]这样的类似数组解包的语法表明接下来可以有任意数量的数字let a: [string, number] = ['Tom', 18] let b: [string, number?] = ['Tom'] let c: [string, ...number[], boolean] = ['Tom', 18, 20, true] c = ['Tom', false]
3.3.7 enum
enum类型表示枚举类型,其中的成员有数字值、字符串值两种形式enum的成员需要用,隔开,一般名称首字母大写enum类型不可修改扩展多个同名的
enum类型会自动合并成一个对象(值覆盖)实际上也可以通过
export obj as const导出一个自定义的对象模拟枚举谨慎使用枚举
- 对于一个枚举类型来说,编译完成后的内容类似这样
// enum Color { // Red, // Green, // Blue // } let Color; (function (Color) { Color[Color.Red = 0] = 'Red' Color[Color.Green = 1] = 'Green' Color[Color.Blue = 2] = 'Blue' })(Color || (Color = {}))- 这带来了许多问题
- 打包体积很大,不适合在项目中大量使用
- 不适合
TreeShaking,存在文件内的全局变量,构建工具可能无法正确处理 enum既可以是类型,也可以是一个值,在引入时会有不必要的依赖
- 替代
- 使用联合类型
type Color = 'Red' | 'Green' | 'Blue'代替,有良好的类型检查,无额外的运行时开销,支持TreeShaking - 使用
as const构建普通变量,并提供可选值类型定义typeof obj[keyof typeof obj],有良好的类型检查,低开销 - 使用
symbol构建(一般不推荐)
- 使用联合类型
// 使用联合类型 type Color = 'Red' | 'Green' | 'Blue' // 使用 as const const Color = { red: 'Red', green: 'Green', blue: 'Blue' } as const; type Color = typeof Color[keyof typeof Color] // => 'Red' | 'Green' | 'Blue'
数字枚举类型定义一组常量,常量的值是固定的,可以增强代码的可读性,避免拼写问题
数字枚举类型定义的内容是一个对象,以你提供的成员名称作为键,对应从
0开始递增的数字作为值。同时也有反向映射,即反过来数字作为键,获取名称字符串可以用
=为enum类型提供显式数字值,这样之后的未提供值的成员值会依次递增值应该独立递增:如果提供的值出现重合,那么对应数字键将对应最后一个值为当前数字的枚举成员,多个枚举成员的值会相同
enum Gender { Male, // 默认从 0 开始递增 Female, } // {0: 'Male', 1: 'Female', Male: 0, Female: 1} console.log(Gender) enum Direction { Up = 1, // 1 Down, // 2 Left, // 3 Right, // 4 } enum Character { A = 3, B = 1, C, D } // {1: 'B', 2: 'C', 3: 'D', A: 3, B: 1, C: 2, D: 3} console.log(Character)
字符串枚举类型需要为成员提供字符串值
字符串类型无法递增,之后的成员需要显式提供值
字符串枚举不同于数字枚举,不会创建反向映射
enum Character { A = "A", B = "B", C = "C", D = 1 } // {1: 'D', A: 'A', B: 'B', C: 'C', D: 1} console.log(Character)
- 常量枚举:常量枚举是
tsc编译器的优化手段,正常的枚举会生成一个对象,但常量枚举为了减少代码量,会将索引值内联到代码中此功能需要跨文件内联,必须设置
isolatedModules: false配置(此配置禁止了额外的tsc支持语法,比如常量枚举),并使用tsc编译才能实现,在babel、esbuild、SWC、vite中无效确保
preserveConstEnums: false,不然也不会擦除常量枚举对象如果无法满足上述要求,这将正常编译为一般的枚举对象
const enum Direction { Up = 1, Down, Left, Right } console.log(Direction.Up) // 编译为 console.log(1)
3.4 type 声明
type声明可以声明一个类型别名,让代码可读性更强,方便类型复用和扩展基本的用法是定义联合类型,联合类型是使用管道符
|连接的多个不同的类型或常量值,表示变量的类型可以是列出的类型中的一种如果
|连接的类型可以是任意的,因此连接两个相同类型或存在包含关系的类型不会报错type HttpStatus = number | string type Gender = 'Male' | 'Female' type Age = 18 | 19 | 20 let status: HttpStatus = 200 let sex: Gender = 'Male' let age: Age = 18
除了联合类型,还有交叉类型,通常用于对象类型,通过
&连接不同的对象类型,要求对象包含之前定义的其他类型的成员的所有属性如果对基本类型使用
&连接,将无法找到满足条件的值,类型推断为never同样的不应该让一个成员在多个交叉的类型中,连接要求这个成员满足多个类型的定义,一般较难满足条件
type Dancer = { dance: ()=>void } type Singer = { sing: ()=>void } type Musician = Dancer & Singer // 需要添加两个类型的成员 let musician: Musician = { dance: ()=>{}, sing: ()=>{} } // 不可能满足条件,没有任何值能够赋值给 a,却要求必须含有成员 a type A = { a: number } type B = { a: string } type C = A & B特殊情况
如上方的函数返回值类型声明
void,根据之前的说法,这里应该没有非undefined返回值,但实际上可以发现,返回任意值都是可以的,看起来好像void类型没有被限制这实际上是
ts为了和js常用的箭头函数同步,箭头函数在只有一句时会返回本句的值,大家也经常使用回调箭头函数的写法const src = [1, 2, 3]; const dst = [0]; // forEach 接受 (v:number)=> void,但 push实际上是有返回值的 src.forEach((el) => dst.push(el));为此,专门对函数类型进行了额外的处理,如果不是在声明函数时提供
void返回值限制,而是先定义变量类型type,在使用时,不会触发警告。但在之后获取到的返回值类型仍是void,依旧不应该依赖它进行之后的处理type Func = ()=> void let fn: Func = ()=> 66 let res = fn() // 报错,无法测试 void 返回值的真实性 if (res) { console.log(res) }具体可见官方文档中的说明
3.5 对类的类型声明
3.5.1 基本的声明
- 对于一个常见的类,在
ts中一般使用class来声明,简单地可以类似js一样使用,但需要标明类型必须写明所有的字段,不能仅通过
this.fieldName = value添加字段需要为所有的字段提供类型声明
需要为构造函数提供所有参数类型声明
最好为继承重写的方法添加
override标识class Person { type = 'person' name: string age: number constructor(name: string, age: number) { this.name = name this.age = age } speak() { console.log(`I'm ${this.name}, I'm ${this.age} years old.`) } } class Student extends Person { grade: number constructor(name: string, age: number, grade: number) { super(name, age) this.grade = grade } override speak() { console.log(`I'm ${this.name}, I'm ${this.age} years old. I'm a student.`) } }typeof 在类型声明中的用法
typeof在ts中实际上可以用在className上,用于获取类定义的构造函数的类型,此方法需要和原始的typeof做区分如果在代码中使用
typeof会正常返回类型字符串如果在
type声明中使用,会获取到类构造函数如果使用类作为函数参数时,会发现得到的推断类型也是
typeof className// 代码中的正常逻辑 if (typeof Person === 'function') {} // 获取到类构造函数的类型 type PersonType = typeof Person // Person 作为参数得到的也是类构造函数,会发现被自动推断为 typeof Person
3.5.2 属性描述符
ts为类提供了额外的属性描述符public:默认属性,类实例、子类和外部都能访问protected:类实例和子类中可访问,外部不可访问private:类实例中可访问,子类和外部不可访问readonly:只读属性,表明属性不能在构造函数外被修改,可和访问范围描述符一起使用。此属性也可用在type和Interface声明中class Person { public name: string protected age: number private readonly birth: string constructor(name: string, age: number, birth: string) { this.name = name this.age = age this.grade = grade } }这些描述符仅在
ts文件中存在,不会有额外的限制代码在编译后的js文件中
属性的简写形式
- 在构造函数初始化时,可以发现进行了大量无关的属性声明,既为所有的字段添加类型声明,又在构造函数通过类型声明并赋值,因此
ts提供了简写形式 - 直接为构造函数添加属性描述符,自动添加对应的字段声明,并进行赋值
- 在父类定义的内容需要调用父类的构造函数
super(),而不是添加描述符
class Person {
constructor(public name: string, public age: number) {}
}
class Student extends Person {
constructor(name: string, age: number, public grade: number) {
super(name, age)
}
}3.5.3 抽象类
许多语言中都有抽象类的概念,在
ts中也提供了抽象类的声明抽象类中可以有仅进行声明但无实现的方法
抽象类不能被实例化,只能被继承
抽象类可以有构造方法和普通方法,也可以有正常的属性
abstract class Person { // 虽然不能实例化,但可以提供构造方法 constructor(public name: string) {} abstract work(time: number):void } // 不可以实例化抽象类 // let person = new Person() // 可以继承抽象类,必须实现方法 class Student extends Person { constructor(name: string, public grade: number) { super(name) } work(time: number): void { console.log(`I'm ${this.name}, I'm working ${time} hours.`) } }
抽象类的用途
- 定义通用接口:为一组相关的类定义通用的行为(方法或属性)
- 提供基础实现:在抽象类中提供某些方法或为其提供基础实现,这样派生类就可以继承这些实现
- 确保关键实现:强制派生类实现一些关键行为
- 共享代码逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复
3.5.4 接口
接口是一种定义数据的方式,主要作用是为类、对象和函数等规定一种契约,保证代码一致性和可维护性
- 与抽象类的区别
- 接口仅能定义格式,不能定义具体的逻辑实现,也不能提供默认值
- 接口可以多继承
- 接口中的属性只能是
public的
- 多个同名的
interface的属性将合并,重名属性以更后定义的为准,合并后的接口才是真实的契约,有类似声明提升的效果 - 实际上也可以和类合并,也就是使用类同名创建的接口,将为类添加额外的属性声明,这样就可以在对实例化的对象添加属性时不报对象上没有这个属性的警告
- 可以使用
readonly和?标识,就像type一样
- 与抽象类的区别
为类定义提供类型声明
接口中不能添加属性描述符,如果提供给类,字段都是
public接口中能不包含对构造方法的限制
interface PersonInterface { public name: string public age: number private idCard: string readonly birth: string printInfo(): void } class Person implements PersonInterface { constructor(public name: string, public age: number, private idCard, public readonly birth: string) {} printInfo() { console.log(`I'm ${this.name}, I'm ${this.age} years old.`) } }
为对象提供类型声明,功能类似
type,但有以下区别二者都可用于声明类型,在许多场景可以互换
interface关注对象和类的结构,支持合并和继承type可以定义交叉类型、联合类型,支持类型别名,但不支持合并(同名报错)和继承type的合并和继承可以通过交叉类型模拟,但interface的写法更容易理解type Person = { name: string age: number idCard?: string readonly birth: string printInfo(): void } interface PersonInterface { name: string age: number idCard?: string readonly birth: string printInfo(): void } // let person: Person = { let person: PersonInterface = { name: 'Tom', age: 18, idCard: '123456789', birth: '1990-01-01', printInfo() { console.log(`I'm ${this.name}, I'm ${this.age} years old.`) } }
定义函数类型:在接口中只有一个无键名的函数声明时,可以将接口作为函数类型使用
如果有多个无键名的函数,以第一个为准
不应该添加额外的键声明
interface FuncInterface { (a:number, b:number):number } let sum: FuncInterface = function(a, b) { return a + b }
接口继承:接口可以继承,进行拓展。支持多继承
interface PersonInterface { name: string age: number } interface ChildrenInterface { isStudy: boolean } interface StudentInterface extends PersonInterface, ChildrenInterface { grade: string } let stu: StudentInterface = { grade: "", name: "", age: 0, isStudy: true, }
- 接口的用途
- 定义数据对象的类型:描述数据模型、
API响应格式等 - 定义类的契约:规定类的属性和方法
- 自动合并:用于扩展第三方库的类型,通过同名接口定义会合并实现
- 定义数据对象的类型:描述数据模型、
3.6 泛型
泛型和许多强类型语言的定义相同,在类型后添加
<T>,就可以引入泛型,意味着有类型不确定,需要声明时提供泛型可以添加到
type、interface、function、class上,并且可以不止一个,多个类型之间以,隔开泛型的字符串
T将作为一个类型,等待接收使用时提供的类型,用于复用逻辑泛型字符串名字不固定,可以自己取名
使用带有泛型类型的声明时,在类型后需要使用
<number>这样的方法提供类型// 泛型接口 interface MessageInterface<T> { info: T } const message:MessageInterface<string> = { info: "" } // 泛型函数 function print<T, U>(a: T, b:U) { console.log(a, b) } print<number, string>(1, "1") // 泛型类 class Message<T> { constructor(public info: T) {} } const messageObj = new Message<string>("message") // 泛型类型 type Info<T> = {info: T} const infoObj: Info<string> = { info:"" }
泛型约束:可以使用
extends关键字要求泛型需要满足的约束条件// 要求是 Person 或是 Person 的子类 function print<T extends Person>(a: T) {} // 要求有 name 和 age 属性 function print<T extends {name: string, age: number}>(a: T) {}
3.7 infer
泛型是期望用户传入类型,实现泛化的类型声明,而
infer就是基于泛化的类型声明通过逻辑获取对应泛型的方法一个简单的应用就是提取类型
通过使用
infer U作为类型,可以获取到对应的类型,存储到U中通过三元组返回获取到的类型
U如果不匹配返回
nevertype ExtractValue<T> = T extends { value: infer U } ? U : never; type A = ExtractValue<{ value: number }> // number
内置的推断类型
ReturnType<T>:获取函数返回值类型Parameters<T>:获取函数参数类型InstanceType<T>:获取构造函数的实例类型- 实例类型和类的类型是不一致的(虽然在编译器中看起来类型是一样的),因此需要此函数转换
- 需要使用
typeof className(获取构造函数类型)或构造函数的泛型T作为输入
ConstructorParameters<T>:获取构造函数的参数type A = ReturnType<() => number> // number type B = ReturnType<(x: string) => Promise<boolean>> // Promise<boolean> type P = Parameters<(a: string, b: number) => void> // [string, number] class A { x = 1; } type I = InstanceType<typeof A> // A function createInstance<T extends new (...args: any[]) => any>( ctor: T, ...args: ConstructorParameters<T> ): InstanceType<T> { return new ctor(...args) as InstanceType<T>; }
4. 导出与导入
4.1 类型声明文件
有许多代码可能使用的是
js编写,如果希望让模块导出的内容有定义,需要添加额外的同名类型声明文件.d.ts可以自由地配置变量属性、函数、类、其他库或全局类型声明
.d.ts文件没有顺序要求,其中的类型声明可以以任意顺序添加文件中使用
export{}表明是模块文件需要import导入对应使用
declare声明类型function声明函数类型,多次声明同函数名可声明重载函数const/let声明一般变量类型,不允许重复声明var声明全局变量类型,重复声明要求都满足,最终报错(一个变量不可能有两个不同的类型)namespace声明命名空间(对应全局变量的属性的类型),相比var,可以方便地命名全局变量的属性类型,namespace还支持声明合并class声明类,不允许重复声明interface声明接口type声明类型别名enum声明枚举
declare function add (a: number, b: number): number; declare const a: string; declare namespace A { function make(s: string): string; let num: number; } export {add, a};
根据上面的介绍,可以发现几乎所有的类型声明,都可以添加到
*.d.ts文件中
- 同样的
ts中也可以使用*.d.ts文件来定义类型,通常用于重定义第三方库的类型,也可以用于定义项目内部的全局类型global声明全局的类型,无论文件是不是模块定义的,其中的内容都会被自动引入- 在
declare global中的内容只要在tsconfig.json中配置的include列表能找到就能被识别到,如果文件不是模块,即没有export {}时就能被自动引入 - 一般还是会添加
export {},这样ts编译器会认为这是一个模块文件,和其他文件一样,类型统一
- 在
module声明模块(常用于覆盖第三方库)- 需要先引入对应的第三方库
- 然后使用同名模块名,使用
declare module声明 - 最后使用
export 类型表示这个扩展的类型可导出,和对应库的原声明一致
声明全局类型export {} declare global { interface Global_Type { num: number firstName: string } var global_var: Global_Type namespace }提供一些部分文件使用的类型// 在运行时可用的 js 类 export class API { constructor(baseURL: string); getInfo(opts: API.InfoRequest): API.InfoResponse; } // 该命名空间与 API 类合并,并允许使用者和当前文件使用嵌套在其中的类型 declare namespace API { export interface InfoRequest { id: string; } export interface InfoResponse { width: number; height: number; } }重定义第三方库的类型// 以 axios 为例,扩展 AxiosRequestConfig 的类型 // 需要引入 axios 库 import "axios" // 重定义 axios 模块中的类型 declare module "axios" { export interface AxiosRequestConfig { skipTransform?: boolean } }
4.2 导入类型
- 类型也可以被导入和导出,对于常见的模块化标准
es2015导出时需要使用
export type导入时需要使用
import typeexport type A = {} import type {A} from "lib"
5. 装饰器
- 装饰器本质上是一种特殊的函数,可以对类、函数、类属性、方法参数等进行扩展
- 第一次提出是在
ES6中,但到现在js仍没有正式支持,目前在ts中也属于测试阶段,需要开启experimentalDecorators选项才能使用(ts 5.0中class装饰器默认可用,其他装饰器还是需要开启这个选项) - 装饰器的语法目前没有大的变化,但不排除未来有进一步更新的可能,文档见Decorators
- 和许多其他的语言一样,类似
java中的注解、python中的装饰器,需要使用@开头 - 装饰器的名称应该对应一种特殊的函数,这个函数有一个变量
target,会接收到被装饰的类或方法
- 第一次提出是在
5.1 类装饰器
- 类装饰器用于为类添加额外的功能,拿取到类之后可以在类上
prototype修改返回值机制
- 如果类装饰器返回一个新类,那么原来的类会被替换为这个新类,新类要求有原始类的所有属性方法,对应参数的类型一致(如果保留参数)
- 如果类装饰器无返回或说返回
undefined,那么类不会发生替换 - 如果返回其他值会报错
一般情况下可以通过
prototype修改类,添加新的实例方法,如果有必要可以通过继承实现新类作为返回值function Demo1(target: new (...args:any[]) => any) { console.log('Demo111') return class extends target { constructor(...args: any[]) { super(...args) console.log('Demo1') } demo() { super.demo() } demo1() { console.log('demo1') } } } @Demo1 class Test { constructor(a: number) { console.log('test') } demo(a: number, b: number) { console.log('classic demo') } }装饰器工厂
因为装饰器本身还不支持传递参数,因此需要传入参数可以考虑使用工厂函数
function DemoFactory(name: string) { return function (target: new (...args:any[]) => any) { console.log(`Demo ${name}`) } } @DemoFactory('张三') class Test {}
5.2 属性装饰器
- 为属性添加装饰器,一般的目的是获取属性被修改的信息
方法的参数为
target(静态属性对应类,实例属性对应类的原型对象prototype),propertyKey(属性名)在构建目标是
es2021及以下版本中,可以使用以下方法修改属性,将实例属性变为get和set方法定义的,具体原因是超过es2021会为编译的文件添加属性声明,导致在实例对象中默认存在这个属性,无法被影响- 使用一个
_变量或weakMap等其他空间存储真实变量,然后就实现了类似vue2劫持的效果 - 可以知道属性被获取和修改,然后做出反应
function test(target: Object, propertyKey: string) { let key = `_${propertyKey}` Object.defineProperty(target, propertyKey, { get() { return this[key] }, set(newValue) { this[key] = newValue console.log("new value is " + newValue) }, enumerable: true, configurable: true }) } class A { @test a:number constructor(a:number) { this.a = a } } let a = new A(1) // A { a: get(), _a: 1, [[prototype]]: {set a(), }, } a.a = 2 console.log(a.a)- 使用一个
5.3 方法装饰器
- 为类中方法添加装饰器,一般用于获取方法被调用的信息
可以实现类似面向切面
AOP的效果,也可以进行方法参数校验方法的参数为
target(静态方法对应为类,实例方法对应类的原型对象prototype),propertyKey(方法名),descriptor方法的(属性描述符),允许返回一个属性描述符,如果未返回会继续使用传入的这个属性描述符属性描述符有一个内置类型
PropertyDescriptor,通过value属性可以获取一般方法本身,如果是访问器get和set方法应该使用get和set属性function Validate(arg: {max?: number, min?: number}) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { // 通过属性描述符获取到方法本身 const originalMethod = descriptor.value; // 修改 descriptor.value = function (...args: any[]) { if (arg.max && args.length > 0 && args[0] > arg.max) { throw new Error(`${propertyKey}的参数1不能大于${arg.max}`); } if (arg.min && args.length > 0 && args[0] < arg.min) { throw new Error(`${propertyKey}的参数1不能小于${arg.min}`); } return originalMethod.apply(this, args); }; return descriptor; }; } function BeforeAspect(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`${propertyKey}方法开始执行`); return originalMethod.apply(this, args); }; } class Person { age: number = 0; @Validate({max: 120, min: 1}) @BeforeAspect setAge(age: number) { this.age = age; } } const person = new Person(); person.setAge(5);
5.4 参数装饰器
- 可以为类中的方法添加参数装饰器,一般用于获取参数信息
- 方法的参数为
target(静态方法对应为类,实例方法对应类的原型对象prototype),propertyKey(方法名),paramIndex(参数索引,从0开始) - 返回值将被忽略
- 参数装饰器先于方法装饰器执行,一般用于存储一些参数信息,用于之后的参数校验
- 经过测试,修改
target[propertyKey]实现额外的函数逻辑不可行,修改不会应用
- 方法的参数为
5.5 注意点
- 装饰器顺序
- 如果一个位置上有多个装饰器,将从上到下依次执行装饰器工厂方法,之后装饰器从下到上依次被触发
- 首先被触发的是实例方法的参数装饰器,随后是实例方法和实例属性的装饰器
- 之后被触发的是静态方法的参数装饰器,随后是静态方法和静态属性的装饰器
- 接下来构造函数的参数装饰器和方法装饰器被触发
- 最后触发类装饰器
- 装饰器会在类的代码声明处执行完毕,因此会在使用前执行
- 有一个
reflect-metadata库,未来可能要作为正式标准的一部分,为反射Reflect添加了额外的方法,可以获取定义装饰器的额外元数据,需要开启emitDecoratorMetadata选项才能使用Reflect.metadata(key, value):是一个装饰器工厂,设置元数据value,key需要和获取时保持一致Reflect.defineMetadata(key, value, target, propertyKey):设置元数据,用于设置对象target的属性propertyKey的元数据Reflect.getMetadata(key, target, propertyKey):获取元数据,第一个key应该和Reflect.metadata中设置的key一致,用于获取对象target的属性propertyKey的元数据,如果当前对象上没有会向上到原型链查找Reflect.getOwnMetadata(key, target, propertyKey):获取对象自身target的属性propertyKey的元数据
