近期一直在使用typescript,仅以此文梳理自己掌握的typescript知识点。
typescript是javascript的超集,它可以编译成javascript在浏览器或node环境中运行。typescript为开发者提供了丰富的数据类型,能够帮助开发人员写出规范且高效的代码。
且因typescript能够引入并使用javascript库,并将其编译成为指定版本的js代码,这使编码过程中可以使用更多新的js特性,而无需担心浏览器兼容性问题。
数据类型
typescript不仅支持javascript中所有的数据类型,另外提供了枚举类型、联合(union type)类型等;在编码过程中,typescript能够实时进行数据类型检查。
-
string
与javascript一样,可以使用''或""表示string,也支持使用``模板字符串:
let username: string = 'reina'; let code: string = "1234"; let email: string = `${username}@gmial.com`; -
number
与javascript一样,在typescript中数字不区分整型和浮点型数据,所有的数字都看作浮点型,另外其也支持二进制、八进制、十六进制的数字:
let userId: number = 1001; let price: number = 55.50; let code: number = 1X90; -
boolean
最简单的true/false数据类型:
let valid: boolean = true; let inValid: boolean = false; -
array
在typescript中也可以使用数组类型,需要指定数组内元素的类型,可以使用“元素类型[]”或“Array<元素类型>”表示:
let list: number[] = [1,2,3]; let items: Array<string> = ['name', 'age', 'title']; -
object
typescript中也可以使用object类型,如以下:
const cource: object = { id:1, title : 'Typescript'};但仅使用object指定类型时, 无法直接使用object.propertyName,如下写法会报错:
const cource: object = { id:1, title : 'Typescript'}; const courseTitle = cource.title; //Error: Property 'title' does not exist on type 'object'.ts(2339)此处可以指定object的内部结构及具体类型:
const cource: {id: number; title: string} = { id:1, title : 'Typescript'}; const courseTitle = cource.title; -
undefined/null
typescript中,null和undefined分别对应各自的类型为null和undefined,应用过程中通常不会定义这两种类型的变量,指定后无法再给变量赋其他值:
let u: undefined = undefined; let n: null = null; -
enum
同Java或C#类似,在typescript中可以使用enum枚举类型,通常当一个变量可能有几种固定的值时,使用枚举类型进行定义,如用户角色:
enum UserRole { ADMIN, READ_ONLY, AUTHOR } let r = UserRole.READ_ONLY;默认枚举类型从0开始编号,相当于:
enum UserRole { ADMIN=0, READ_ONLY=1, AUTHOR=3 }当然,也可以指定任意需要的值:
enum UserRole { ADMIN='admin', READ_ONLY=100, AUTHOR=300 } let r = UserRole.ADMIN; -
any
Any mean you can do whatever you want. 在编码时,如果无法确定变量类型时,可以使用any,即表示该变量可以为任何类型,可以赋任何类型的值,通常作为作为一种fall back,应尽量避免使用:
let value: any; value = 'string value'; value = 100; value = []; value = false; -
Function
Function表示函数类型,可通过Function关键字进行指定其为函数类型,也可通过“参数类型 => 返回值类型”进行指定:
let add: Function; let add: (a: number, be: number) => number; -
void
void表明函数没有任何返回值,与any可以表示任意类型相反,void不表示任何类型,如:
function log(): void { console.log('anything') } -
tuple
tuple翻译为元组类型,表示一个数组已知其元素的类型和数量,但是元素类型不相同,如:
let t :[string, number]; t = ['test', 1]; t = ['test'] // Error t = [1, 'test'] // Error -
union type联合类型
当变量可能是某两种或多种类型时,可以使用联合类型,用‘|’连接多个类型,如id可能是string,也可能是number类型时:
let id: number | string; id = 1001; id = '1001'; -
never
never同字面意思一致,表示永不存在的值的类型,如error函数的返回值,即不为any,也不是void,函数具有永远无法达到的终点,定义为never:
function error(): never{ throw new Error('Something Wrong...') }
Compile config
-
tsconfig.json
typescript必须编译成javascript,才能在浏览器或node环境中运行。typescript项目中,tsconfig.json中指定了用来编译这个项目的根文件和编译选项,可以根据需要进行配置。
可以通过tsc 文件名对指定的文件进行编译,此时会忽略tsconfig.json文件的配置。
当只使用tsc命令,默认会将根目录(tsconfig.json所在目录)下的所有ts文件,按照tsconfig.json中设定的规则进行编译。
使用tsc --init会生成包含默认设置的tsconfig.json文件,默认生成的文件中包含compilerOptions的各个配置项及其简要说明。 -
files, include和exclude
除compilerOptions中的内容外,还可以通过files, include和exclude指定需要编译的具体文件或文件范围。如指定编译src目录下的文件,并排除node_modules文件夹和测试文件:
"include": [ "src/**/*" ], "exclude": [ "node_modules", "**/*.spec.ts" ] 需要注意的是node_modules默认是被exclude,通常无需再额外配置。
若使用files,则需要指定被具体的被编译的文件列表:
"files": [ "./src/core.ts", "./src/sys.ts", "./src/types.ts", "scanner.ts", "parser.ts", "utilities.ts" ] -
关于compilerOptions
仅列举几个经常用到的options:
target:指定编译成哪个版本的js,可指定为'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017'等。
lib: 编译过程中需要引入的库。
sourceMap: 是否生成map文件,生成后支持debug ts代码。
outDir: 输出js的目录。
rootDir: 指定根目录。
removeComments: 编译过程中移除注释代码。
class
-
javascript中的类
在es6之前的javascript中,类是通过构造函数+原型链实现的,如:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function () { console.log('hello ' + this.name); }; let ann = new Person('Ann', 20); ann.sayHello();在es6中,正式引入了class的概念,可以使用class关键字定义类:
class Person { constructor(name, age){ this.name = name; this.age = age; } sayHello(){ console.log(`Hello ${this.name}`); } } const ann = new Person('Ann', 20); ann.sayHello(); -
typescript中的class
而在typescript中,直接定义class的方式与es6类似:
class Person { constructor(private name: string, private age: number){} // 相当于先声明私有的name、age,再通过参数赋值 sayHello():void{ console.log(`Hello ${this.name}`); } } const ann = new Person('Ann', 20); ann.sayHello();将上述ts代码编译为es5的js代码,如下:
var Person = /** @class */ (function () { function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function () { console.log("Hello " + this.name); }; return Person; }()); var ann = new Person('Ann', 20); ann.sayHello(); -
Class的继承
在面向对象模式中,继承是非常常用的,typescript中同样可以extends关键字实现类的继承:
class Girl extends Person { gender: string; constructor(name: string, age: number){ super(name, age); //调用基类的构造函数 this.gender = 'female'; } } const g = new Girl('Jean', 18); g.sayHello(); console.log(g.gender);上述代码中,Girl类作为派生类,从基类Person中继承了属性了方法,并在子类中定义了自己的gender属性;需要注意的是,当派生类中包含构造函数使,它 必须调用
super(),该方法会执行基类的构造函数 -
private, public and protected
在typescript中,也可以使用private、public和protected对类内的成员变量进行标记:
- private用来标记私有成员,即只能在类的内部使用;
- public用来标记公有成员,在类内、类外都可使用,typescript默认成员都为共有的;
- protected标记的成员,不仅在该类内可以访问,在该类的派生类中也是可以访问的。
-
readonly
readonly关键字能够把变量标记为只读的,该变量只能在函数声明时或构造函数中赋值一次,不能再次赋值,使用readonly修饰符能够保证变量在其生命周期内值的唯一性:
class Girl extends Person { public readonly gender: string = 'female'; constructor(name: string, age: number){ super(name, age); } } const g = new Girl('Jean', 18); g.gender = 'male'; //Error: Cannot assign to 'gender' because it is a read-only -
访问器(Getter 和 setter 方法)
TypeScript支持通过getters/setters来截取对对象成员的访问,通过getter/setter能够实现对私有成员的访问与赋值,而不需要我们将所有的成员变量都设为public。
通过getter获取私有成员信息:
class Person { private _id: number; get id(): number{ return this._id; } constructor(private name: string) { this._id = 123456; } } let ann = new Person('Ann'); console.log(ann.id);//当执行ann.id时,会调用get方法,返回类内私有成员_id的信息。通过setter设置私有成员变量:
class Person { private _id: number; get id(): number{ return this._id; } set id(newId: number){ this._id = newId; } constructor(private name: string) { this._id = 123456; } } let ann = new Person('Ann'); console.log(ann.id); ann.id = 654321; //与get同理,此处赋值时会执行set方法,并通过set方改变私有成员_id的信息 console.log(ann.id); -
Static method静态方法
typescript支持创建类的静态成员,即属性或方法仅存在于类本身,而不依附于类的实例,使用static关键字来定义静态成员:
class Group { static createMember (name: string) { return {groupName: name}; } static groupId = '01'; } const group = Group.createMember('group-A'); const groupId = Group.groupId;上述代码在Group类内定义了静态方法createMember和静态属性groupId,可以通过“类名.方法名”或“类名.属性名”直接使用。
-
abstract class抽象类
typescript中,抽象类是一种不能实例化的类,通常作为派生类的基类,使用abstract关键字定义抽象类,在抽象类的内部,可以定义派生类的成员进行约束:
abstract class Animal { abstract runSpeed: number; //派生类中的必有属性 abstract eat(): void; //派生类中的必须重写的方法 } // const a = new Animal(); // Cannot create an instance of an abstract class. class Dog extends Animal { runSpeed = 100; eat() { console.log('Eat food.') } } const d = new Dog() console.log(d.runSpeed); d.eat(); 在上述代码中,通过abstract关键字定义了Animal类为抽象类,该类不能直接实例化;另外,在Animal类内定义了抽象的属性runSpeed和方法eat,在定义派生类中,必须具有runSpeed属性,并必须重写run方法。
需要注意的是,抽象属性或抽象方法只能在抽象类中存在。也就是说,一个类不能既支持实例化,又支持抽象属性或方法。
-
singletons class单例类(private constructor)
除了抽象类,typescript中同样支持创建单例类。跟字面意思一致,该类有且只有一个实例,通过构造函数的私有化实现:
class Group { private static instance: Group; private constructor(){} static getInstance(){ if(this.instance){ return this.instance } else { this.instance = new Group(); // 在类内进行实例化 return this.instance; } } } console.log(Group.getInstance()); // 通过静态方法获取类的实例上述代码中,将Group类定义为单例类。首先定义了私有化的构造函数,并在类的内部进行实例化;这样,在类的外部无法通过new关键字再次实例化该类,也就保证了Group类有且只有一个实例。另外,在类中定义了getInstance()静态方法返回唯一的实例,在类的外部,可以通过静态方法获取类的实例。
interface
在面向对象模式中,可以将接口看作一个特殊的抽象类;typescript中,接口通常用于定义一个对象的结构,接口中不包含任意初值或方法,只定义该对象应该具有哪些属性或方法及其类型;typescript中定义的接口,编译成javascript之后是完全透明的。
- 定义一个简单的接口并使用它:
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
const ann: Person = {
name: 'Ann',
age: 20,
greet(phrase: string) {
console.log(phrase);
}
}
上述代码定义了一个Person接口,并定义了其内部的属性和方法;在构造ann对象时,它必须包含string类型的name属性、number类型的age属性和没有返回值的greet()方法。
-
可选的属性或方法
typescript中可以使用
属性名?: 数据类型或方法名?(参数):返回值的形式定义可选属性或方法:interface Person { name: string; age?: number; greet?(phrase: string): void; } const ann: Person = { name: 'Ann', } -
只读属性
在属性名前使用
readonly关键字进行标记,表明该属性是只读的,只能在创建对象时赋值,不可更改:interface Person { name: string; readonly age: number; } const ann: Person = { name: 'Ann', age: 20 } ann.age = 30 //Cannot assign to 'age' because it is a read-only property.ts -
函数类型的接口
接口除了能够描述object内的数据结构,也可用来描述函数类型:
interface AddFn { (a: number, b: number) : number; } const add: AddFn = (a: number, b: number) => { return a + b; }上述代码定义了AddFn作为函数Add的接口,描述了其参数和返回值的具体类型。
除了使用接口外,还可以使用type关键字,为函数的类型提供一个别名:
type AddFn = (a: number, b: number) => number; const add: AddFn = (a: number, b: number) => { return a + b; } -
类的接口
TypeScript中也可以使用接口约束某一个类的结构,使用implements关键字:
interface User { name: string; email: string; login(password: string): boolean; } class Admin implements User { name: string; email: string; constructor(n: string, e: string) { this.name = n; this.email = e; } login(password: string) { return true; } }需要注意的是,类的接口只描述类的共有成员,而不会检测私有成员;若在类中将name定义为私有属性,则会报错,显示缺少了name属性。
-
接口的继承
和类一样,接口也可以相互继承;利用接口的继承,使用extends关键字可以将一个接口中成员复制到另一个接口中:
interface Admin { id: number; } interface Vistor { name: string; email: string; } interface User extends Admin, Vistor { password: string; } let user: User = { id: 1001, name: 'Ann', email: 'XX@gmial.com', password: '123456' }上述代码中,User接口同时继承了Admin和Vistor接口,它同时具有了两个接口的成员和它自己的成员。
进阶用法
-
交叉类型(intersection type)
交叉类型可以把多个类型通过
&操作符合并为一种类型,合并后的类型拥有合并前所有了类型的特性。type A = { name: string; age: number; } type B = { name: string; birthday: Date; } type C = A & B; const obj: C = { name: 'Roy', age: 20, birthday: new Date() }上述代码C是由A、B组成的交叉类型,obj作为C类型的对象,其既具有A中的属性,也具有B类型中的属性,且相同的name属性会合并保留一个。
-
类型别名
typescript支持使用type关键字为任何类型创建别名,尤其在使用联合类型时,可以简化代码,如用Code类型表示即可以是number,也可以是string的类型:
type Code = string | number; let statusCode: Code = 404; statusCode = '503'; -
联合类型(union type)
与交叉类型不同,联合类型表示一个类型可以是几种类型之一,使用
|操作符;如当用户手机号可能是数字也可能是string时:type PhoneNum = string | number;又如:
type A = { name: string; age: number; } type B = { name: string; birthday: Date; } type D = A | B; let obj: D = { name: 'Roy', age: 18 } obj = { name: 'Roy', birthday: new Date() }上述代码中,obj即可以是A类型,也可以是B类型。
-
类型保护(Type Guard)
当使用联合类型的变量时,只能使用多个类型中的共同成员;否则代码或报错,如:
type Num = string | number; function split(phoneNum: Num) { phoneNum.split('') // Error: Property 'split' does not exist on type 'Num'. // Error: Property 'split' does not exist on type 'number'. }上述代码中,定义一个简单的split方法,接收string或number类型的参数,若在函数内部直接调用split方法代码会报错,这时候需要类型保护。
- typeof类型保护
type Num = string | number; function split(phoneNum: Num) { if (typeof phoneNum === 'number') { return phoneNum.toString().split('') } else { return phoneNum.split('') } }重构split方法,并在方法体中使用typeof进行类型的判断,代码可以正常的编译且执行;但是,typeof 只能判断number、string、boolean和symbol类型的数据,对于其他类型的数据,typeof无法正确的进行判断。
- in 类型保护
如以下代码,printInfo方法接收一个D类型的参数,D类型是A和B组成的联合类型,name作为A和B类型中的共有成员,可以在函数体内直接使用;而直接使用obj.birthday时,因birthday只在B类型中存在,代码显示Error信息:
type A = { name: string; age: number; } type B = { name: string; birthday: Date; } type D = A | B; function printInfo(obj: D){ console.log(obj.name); console.log(obj.birthday) // Error:Property 'birthday' does not exist on type 'D'. // Error:Property 'birthday' does not exist on type 'A'. }这种情况无法使用typeof判断obj到底是A类型还是B类型,可以使用in操作符,使用
propertyName in object的形式判断其成员:function printInfo(obj: D) { console.log(obj.name); if ('birthday' in obj) { console.log(obj.birthday); } if ('age' in obj) { console.log(obj.age); } }- instanceof类型保护
除了上述方法外,还可是使用instanceof判断对象的构造函数,对变量的具体类型进行区分。如下面的代码,将类当作接口,在print方法中,使用instanceof判断ocj的类型:
class Admin { id: number; } class Vistor { name: string; email: string; } function print (obj: Admin | Vistor){ if(obj instanceof Admin){ console.log(obj.id); } if(obj instanceof Vistor){ console.log(obj.name + obj.email); } } -
可辨识联合(Discriminated Unions)
除了上述的类型判断方式,typescript支持创建一种“可辨识联合”的高级类型,在该类型中结合了单例类型,联合类型,类型保护和类型别名。示例如下:
interface Bird { kind: 'bird'; flyingSpeed: number; } interface Horse { kind: 'horse'; runningSpeed: number; } type Animal = Bird | Horse; function showSpeed(animal: Animal) { let speed: number; if (animal.kind === 'bird') { speed = animal.flyingSpeed; } else { speed = animal.runningSpeed; } console.log(speed); } const animal: Animal = { kind: 'bird', flyingSpeed: 100 } showSpeed(animal);上述代码中,首先定义了将要联合的两个接口,每个接口中都有kind属性,但赋给其不同的字面量值,此时的kind属性就作为可辨识的标签;然后通过类型别名将这两个接口联合到一起;在showSpeed方法中,可直接使用标签进行判断。
-
类型断言
在某些情况下,typescript获取的数据类型并不是最精准的;如从DOM结构中获取input元素,typescript默认取得的元素类型是HTMLElement,而实际上可以确认元素是HTMLInputElement类型,此时,可以使用类型断言,相当于告诉typescript「我确信它是HTMLInputElement类型」:
let element_1 = document.getElementById('input'); const val_1 = element_1.value; //Error: Property 'value' does not exist on type 'HTMLElement'. const element_2 = document.getElementById('input') as HTMLInputElement; // 使用as操作符 const val_2 = element_2.value;除了上述使用as进行断言外,还可使用以下形式:
const element_2 = <HTMLInputElement>document.getElementById('input'); const val_2 = element_2.value;类型断言只在编译时发挥作用,代码运行过程中不会产生任何影响。
-
索引类型
在typescript中使用索引类型,可以动态的添加和使用任意属性名。如当需要为validation提供一个Error Message的集合:
interface ErrorContainer { [prop: string]: string; } const errorMsg: ErrorContainer = { name: 'Name is required!', password: 'Password should be more than 6 digits!' // ...... 可任意添加 } -
函数重载
函数的重载指的是两个以上的函数,具有相同的函数名,但是形参的个数或者参数的类型不同,编译器可以根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数。
在javascript中,后面定义的函数会覆盖前面的同名函数,因此javascript不支持函数重载;但在typescript中是可以实现函数重载:
type universal = number | string; function add(a: number, b: number): number; function add(a: string, b: string): string; function add(a: string, b: number): string; function add(a: number, b: string): string; function add(a: universal, b: universal) { if (typeof a === 'string' || typeof b === 'string') { return a.toString() + b.toString(); } else return a + b; } add('1', 2).split(''); add(1,3).toFixed(); add(1,3).split(''); //Error: Property 'split' does not exist on type 'number'上述代码中,执行add('1', 2)能够自动匹配参数类型,且已知返回值是string类型,因此可以直接调用string.split方法;同理执行add(1,3)能确定返回值是number类型,能够直接调用Number.toFixed方法,而调用String.split方法报错。
-
?.操作符(Nullish Coalescing)在typescript 3.7版本中,加入了
?.操作符,用C#的同学应该非常熟悉,使用?.可以简化null和undefined的判断:const title = userInfo?.job?.title; // 相当于 const title = userInfo && userInfo.job && userInfo.job.title; -
??操作符(Optional Chaining同时,在typescript 3.7版本中新添加了
??操作符,可以将??看作当变量值为undefined或null时设置默认值的fallback,需要注意仅有变量为undefined或null时才会执行default,空字符串('')能够正常赋值:let userInput; let saveVal = userInput ?? 'Default'; // Default userInput = null; saveVal = userInput ?? 'Default'; // Default userInput = ''; saveVal = userInput ?? 'Default'; // ''
范型
在软件开发过程中,代码通用性是必须要考虑的,不管是组件的通用型,还是类或方法的通用型;编码过程中,不仅要考虑对当前数据类型的支持,也要考虑对将来其他数据类型的支持。
在C#或Java中,可以使用范性创建可通用的组件,使一个组件能够支持多种数据类型,在typescript中,同样可以使用范性优化代码。
-
什么是范型
可以将范型看作一种类型变量,它是一种特殊的变量,只用于表示类型而不是值。
下面定义一个简单的方法,可以接受接受任意类型的参数,并返回参数本身:
function identity(arg: any): any{ return arg; } const n: number = identity('123');上例中使用了
any表示参数的类型及返回值的类型,但是已知any可以表示任何类型,若传入string类型的参数,并将返回值指定为number类型,并不会显示错误,可见使用any,无法保证类型唯一。此时可以使用类型变量
T,确保传入参数和返回值同属于T类型:function showVal<T>(arg: T): T{ return arg; } const s: string = showVal('abc'); const n: number = showVal(123); const m: number = showVal('123'); //Error: Type '"123"' is not assignable to type 'number' -
使用多个范型变量
function merge<T, U>(objA: T, objB: U): T & U{ return Object.assign(objA, objB); } const mergedObj = merge({name: 'Max'}, {age: 20}); const name = mergedObj.name; const age = mergedObj.age; const obj = merge({name: 'Max'}, 30);上述代码中使用了两个范型,并且返回值为两个范型的交叉类型,可以直接获取返回值的name、age属性;但是,上述代码中仅指定了参数和返回值范型变量,无法保证传入的参数都是object类型,若传入其他类型的参数,如
merge({name: 'Max'}, 30)代码并不会报错。 -
范型约束
在之前定义的merge方法中,可以使用extends关键字为范型变量添加约束;下面的代码将范型变量
T和U都限制为object类型,此时再传入其他类型的参数,代码将报错:function merge<T extends object, U extends object>(objA: T, objB: U): T & U { return Object.assign(objA, objB) } const mergedObj = merge({name: 'Max'}, 30); // Error: Argument of type '30' is not assignable to parameter of type 'object'.除了使用object这种原生类型来添加范型约束外,也可以使用接口来描述约束条件,如定义一个含length属性的接口,要求传入的参数必须具有length属性:
interface Lengthy { length: number; } function countAndDescribe<T extends Lengthy>(element: T): [T, string] { let desc = 'Got no value'; if (element.length === 1) { desc = 'Got 1 element'; } else if (element.length > 1) { desc = 'Got ' + element.length + ' elements'; } return [element, desc]; } countAndDescribe('abc'); countAndDescribe([1, 2, 3]); countAndDescribe(100); // Error: Argument of type '100' is not assignable to parameter of type 'Lengthy'. -
keyof
除了上述的范型约束外,还可以使用keyof关键字在两个范型之间建立约束。
下面定义了一个简单的方法,通过属性名获取对象中相应的属性值,
getValue({}, 'name'),编译时会报错,因为name属性在{}中并不存在:function getValue(obj: object, key: string) { return obj[key]; } getValue({}, 'name');使用keyof关键字建立两个参数之间的约束:
function getValue<T extends object, U extends keyof T>(obj: T, key: U) { return obj[key]; } getValue({}, 'name'); // Error: Argument of type '"name"' is not assignable to parameter of type 'never'.上述代码中,
T extends object限制第一个参数为任意的object,而U extends keyof T则约束第二个参数为第一个参数的任意一个key,此时getValue({}, 'name')将在编码时显示错误。 -
范型类
在typescript中,可以使用范型定义类:
class dataStorage<T extends string | number | boolean>{ private data: T[] = []; addItem(item: T): void { this.data.push(item); } removeItem(item: T) { const index = this.data.indexOf(item); if (index !== -1) { this.data.splice(index, 1); } } } const textStorage = new dataStorage<string>() textStorage.addItem('stringVal'); const numStorage = new dataStorage<number>() numStorage.addItem(100); const objStorage = new dataStorage<object>() // Error上述代码中,定义了通用的dataStorage方法,可在实例化时指定具体的元素类型;使用
T extends string | number | boolean约束范型,无法使用object类型进行实例化。 -
Partial< T >
Partial能够构造一个T类型的子集,子集中将T的所有属性设置为可选:
interface Person{ name: string, age: number } function createPerson(): Person{ const p: Person = {}; // Error: Type '{}' is missing the following properties from type 'Person' p.name = 'Max'; p.age = 18; return P; }上述代码中,想要先构建一空的Person对象,再向对象添加属性值,代码报错,此时可以使用Partial,临时将Person对象中的属性设为可选的,最后再使用as关键字进行转换类型:
function createPerson(): Person{ const p: Partial<Person> = {}; p.name = 'Max'; p.age = 18; return p as Person; }
装饰器 Decorators
在代码运行过程中动态的增加功能的方式,叫做装饰器。typescript中支持使用装饰器,前提是将compilerOptions 中的experimentalDecorators设为true。
装饰器是一种特殊类型的声明,它能够被附加到类的声明、方法、属性、getter/setter或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,该函数在运行时被调用,被装饰的声明信息作为参数传入。
-
类装饰器(class decorators)
function Logger(constructor: Function){ console.log('This is a decorator!') } @Logger class Person { name: string = 'Max'; constructor(){ console.log('calling person constructor!') } } console.log('before creating the person instance!'); const p = new Person();上述代码中定义了Person类的装饰器Logger,类装饰器仅接收一个参数,即为类的构造函数,无法传递其他参数;且类装饰器会在类声明之前被调用,运行结果如下:
This is a decorator! before creating the person instance! calling person constructor! -
装饰器工厂(decorator factories)
在定义装饰器时,可以使用装饰器工厂,使用装饰器工厂能够自定义的向装饰器传递参数;装饰器工厂就是一个简单的表达是,它返回一个函数,在装饰器运行时调用。如下重构Logger装饰器,并向其传参:
function Logger(logMsg: string) { return function (constructor: Function) { console.log(logMsg); } } @Logger('Loading Person Class.') class Person { name: string = 'Max'; constructor() { console.log('calling person constructor!') } } console.log('before creating the person instance!'); const p = new Person();运行结果:
Loading Person Class before creating the person instance! calling person constructor!利用装饰器工厂,可以实现很多强大的功能,比如Angular在框架中,根据template动态创建component的过程:
function WithTemplate(template: string, hostId: string){ return function(constructor: any){ const hostEle = document.getElementById(hostId); const component = new constructor(); if(hostEle){ hostEle.innerHTML = template; hostEle.querySelector('h1')!.textContent = component.name; } } } @WithTemplate('<h1>Title</h1>', 'app') class Component { name: string = 'Component by decorator'; constructor(){} }上述代码中,在声明component类时,创建了装饰器工厂,并向其传递了两个参数,生成DOM结构的template(
<h1>Title</h1>)和父节点的id(app);在装饰器工厂中,通过constructor实例化一个Component对象,获取类中name属性插入到<h1>标签中。Component类声明时,<h1>Component by decorator</h1>会渲染到在id为app的DOM元素中。 -
多个装饰器组合
在同一个类声明时,可以使用多个装饰器。下面定义了两个装饰器Logger_1和Logger_2:
function Logger_1(constructor: Function) { console.log('This is a logger_1!') } function Logger_2(constructor: Function) { console.log('This is a logger_2!') } @Logger_1 @Logger_2 class Person { name: string = 'Max'; constructor() { console.log('calling person constructor!') } } console.log('before creating the person instance!'); const p = new Person();运行上述代码,控制台输出顺序如下:
This is a logger_2! This is a logger_1! before creating the person instance! calling person constructor!可见两个装饰器都是在类声明前调用的,且Logger_2先于Logger_1被调用,可以理解为装饰器是自下而上执行的。
改造上述的两个装饰器,使其都返回一个方法,对比多个装饰器工厂的执行顺序,可以发现装饰器工厂的声明是自上而下进行的,而返回的表达式仍是自下而上执行的。
function Logger_1() { console.log('Declear logger_1 factory') return function (constructor: Function) { console.log('Exclude logger_1!') } } function Logger_2() { console.log('Declear logger_2 factory') return function (constructor: Function) { console.log('Exclude logger_2!') } } @Logger_1() @Logger_2() class Person { name: string = 'Max'; constructor() { console.log('calling person constructor!') } } console.log('before creating the person instance!'); const p = new Person();控制台输出顺序:
Declear logger_1 factory Declear logger_2 factory Exclude logger_2! Exclude logger_1! before creating the person instance! calling person constructor! -
属性装饰器
除了类装饰器外,也可为属性添加装饰器,同样,属性装饰器也是在属性声明前调用的;属性装饰器接受两个参数,第一个参数为类的构造函数(静态成员属性)或类的原型对象(实例成员属性);第二个参数为属性名。
function Logger(target: any, propertyName: string) { console.log(target); // Person {} console.log(propertyName);// name } class Person { @Logger name: string; constructor() { this.name = 'Max'; } } -
访问器装饰器
typescript中,访问器装饰器通常指的是在声明get或set访问器时,调用的装饰器。对于同一个属性,不能同时给get和set添加装饰器。访问器装饰器接收三个参数:第一个参数为类的构造函数(静态成员)或类的原型对象(实例成员);第二个参数为属性名; 第三个为成员描述符(PropertyDescriptor):
function Logger(target: any, propertyName: string, descriptor: PropertyDescriptor) { console.log(target); // Person {} console.log(propertyName); // name console.log(descriptor); // { get: [Function: get name], // set: undefined, // enumerable: false, // configurable: true } } class Person { private _name: string; @Logger get name(){ return this._name; } constructor() { this._name = 'Max'; } } -
方法装饰器
方法装饰器在方法的声明前调用,可以用来监视,修改或替换方法的定义。与访问器装饰器一样接收三个参数,分别为:类的构造函数(静态成员)或类的原型对象(实例成员);方法名和成员描述符(PropertyDescriptor):
function Logger(target: any, propertyName: string, descriptor: PropertyDescriptor) { console.log(target); // Person{} console.log(propertyName); // showName console.log(descriptor); // { value: [Function: showName], // writable: true, // enumerable: false, // configurable: true } } class Person { private name: string; constructor() { this.name = 'Max'; } @Logger showName() { console.log(this.name) } } -
参数装饰器
除了在方法上使用装饰符外,也可在方法的参数上使用装饰符,参数装饰器声明在一个参数声明之前,应用于类构造函数或方法声明时;参数装饰符接收三个参数,分别为:类的构造函数(静态成员)或类的原型对象(实例成员);方法名和参数在函数参数列表中的索引。下面的示例中,给showName方法的第二个参数添加装饰符:
function Logger(target: any, propertyName: string, idx: number) { console.log(target); // Person{} console.log(propertyName); // showName console.log(idx); // 1 } class Person { name: string; age: number; constructor() { this.name = 'Max'; this.age = 18; } showName(name: string, @Logger age: number) { console.log(this.name) } } -
Autobind - Method decorator demo
typescript提供了各式各样的装饰符,利用这些装饰符可以用来用来监视,修改或替换类、方法、属性等,利用装饰器,可以灵活的实现很多功能,下面构建一个方法装饰器,实现事件监听回调方法中this的正确绑定。
class Person { age: number; constructor() { this.age = '18; }; showName() { console.log(this.age); } } const p = new Person(); let btn = document.getElementById('submit')!; btn.addEventListener('click', p.showName) // undefined上述代码中,为submit添加了事件监听,并在click 触发式调用p对象的showName方法,此时,当showName运行时,this指向的是button对象而不是p对象,只会输出undefined。
通常情况下,会使用bind方法,手动将this绑定到p对象上:
btn.addEventListener('click', p.showName.bind(p)); // 18定义一个方法装饰器,使this能够绑定到正确的对象上;在方法装饰器中,首先通过成员的属性描述符PropertyDescriptor,获取到初始的方法;然后,重组一个成员描述符,在其get方法中返回绑定this后的方法;再将新的成员描述符作为装饰器的返回值:
function AutoBind(_: any, _2: string, descriptor: PropertyDescriptor) { const originMethod = descriptor.value; const adjustedMethod: PropertyDescriptor = { enumerable: false, configurable: true, get() { return originMethod.bind(this); } } return adjustedMethod; } class Person { age: string; constructor() { this.age = '18'; }; @AutoBind showName() { console.log(this.age); } } const p = new Person(); let btn = document.getElementById('submit')!; btn.addEventListener('click', p.showName); // 18 ```