typescript常用知识点梳理

·  阅读 974

近期一直在使用typescript,仅以此文梳理自己掌握的typescript知识点。

typescript是javascript的超集,它可以编译成javascript在浏览器或node环境中运行。typescript为开发者提供了丰富的数据类型,能够帮助开发人员写出规范且高效的代码。

且因typescript能够引入并使用javascript库,并将其编译成为指定版本的js代码,这使编码过程中可以使用更多新的js特性,而无需担心浏览器兼容性问题。

数据类型

typescript不仅支持javascript中所有的数据类型,另外提供了枚举类型、联合(union type)类型等;在编码过程中,typescript能够实时进行数据类型检查。

  1. string

    与javascript一样,可以使用''或""表示string,也支持使用``模板字符串:

    let username: string = 'reina';
    let code: string = "1234";
    let email: string = `${username}@gmial.com`;
    复制代码
  2. number

    与javascript一样,在typescript中数字不区分整型和浮点型数据,所有的数字都看作浮点型,另外其也支持二进制、八进制、十六进制的数字:

    let userId: number = 1001;
    let price: number = 55.50;
    let code: number = 1X90;
    复制代码
  3. boolean

    最简单的true/false数据类型:

    let valid: boolean = true;
    let inValid: boolean = false;
    复制代码
  4. array

    在typescript中也可以使用数组类型,需要指定数组内元素的类型,可以使用“元素类型[]”或“Array<元素类型>”表示:

    let list: number[] = [1,2,3];
    let items: Array<string> = ['name', 'age', 'title'];
    复制代码
  5. 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;
    复制代码
  6. undefined/null

    typescript中,null和undefined分别对应各自的类型为null和undefined,应用过程中通常不会定义这两种类型的变量,指定后无法再给变量赋其他值:

    let u: undefined = undefined;
    let n: null = null;
    复制代码
  7. 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;
    复制代码
  8. any

    Any mean you can do whatever you want. 在编码时,如果无法确定变量类型时,可以使用any,即表示该变量可以为任何类型,可以赋任何类型的值,通常作为作为一种fall back,应尽量避免使用:

    let value: any;
    value = 'string value';
    value = 100;
    value = [];
    value = false;
    复制代码
  9. Function

    Function表示函数类型,可通过Function关键字进行指定其为函数类型,也可通过“参数类型 => 返回值类型”进行指定:

    let add: Function;
    let add: (a: number, be: number) => number;
    复制代码
  10. void

    void表明函数没有任何返回值,与any可以表示任意类型相反,void不表示任何类型,如:

    function log(): void {
      console.log('anything')
    }
    复制代码
  11. tuple

    tuple翻译为元组类型,表示一个数组已知其元素的类型和数量,但是元素类型不相同,如:

    let t :[string, number];
    t = ['test', 1];
    t = ['test'] // Error
    t = [1, 'test'] // Error
    复制代码
  12. union type联合类型

    当变量可能是某两种或多种类型时,可以使用联合类型,用‘|’连接多个类型,如id可能是string,也可能是number类型时:

    let id: number | string;
    id = 1001;
    id = '1001';
    复制代码
  13. never

    never同字面意思一致,表示永不存在的值的类型,如error函数的返回值,即不为any,也不是void,函数具有永远无法达到的终点,定义为never:

    function error(): never{
      throw new Error('Something Wrong...')
    }
    复制代码

Compile config

  1. tsconfig.json
    typescript必须编译成javascript,才能在浏览器或node环境中运行。typescript项目中,tsconfig.json中指定了用来编译这个项目的根文件和编译选项,可以根据需要进行配置。
    可以通过tsc 文件名对指定的文件进行编译,此时会忽略tsconfig.json文件的配置。
    当只使用tsc命令,默认会将根目录(tsconfig.json所在目录)下的所有ts文件,按照tsconfig.json中设定的规则进行编译。
    使用tsc --init会生成包含默认设置的tsconfig.json文件,默认生成的文件中包含compilerOptions的各个配置项及其简要说明。

  2. 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"
    ]
    复制代码
  3. 关于compilerOptions
    仅列举几个经常用到的options:
    target:指定编译成哪个版本的js,可指定为'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017'等。
    lib: 编译过程中需要引入的库。
    sourceMap: 是否生成map文件,生成后支持debug ts代码。
    outDir: 输出js的目录。
    rootDir: 指定根目录。
    removeComments: 编译过程中移除注释代码。

class

  1. 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();
    复制代码
  2. 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();
    复制代码
  3. 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(),该方法会执行基类的构造函数

  4. private, public and protected

    在typescript中,也可以使用private、public和protected对类内的成员变量进行标记:

    • private用来标记私有成员,即只能在类的内部使用;
    • public用来标记公有成员,在类内、类外都可使用,typescript默认成员都为共有的;
    • protected标记的成员,不仅在该类内可以访问,在该类的派生类中也是可以访问的。
  5. 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 
    复制代码
  6. 访问器(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);
    复制代码
  7. 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,可以通过“类名.方法名”或“类名.属性名”直接使用。

  8. 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方法。

    ​ 需要注意的是,抽象属性或抽象方法只能在抽象类中存在。也就是说,一个类不能既支持实例化,又支持抽象属性或方法。

  9. 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之后是完全透明的。

  1. 定义一个简单的接口并使用它:
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()方法。

  1. 可选的属性或方法

    typescript中可以使用属性名?: 数据类型方法名?(参数):返回值的形式定义可选属性或方法:

    interface Person {
      name: string;
      age?: number;
      greet?(phrase: string): void;
    }
    
    const ann: Person = {
      name: 'Ann',
    }
    复制代码
  2. 只读属性

    在属性名前使用 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
    复制代码
  3. 函数类型的接口

    接口除了能够描述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;
    }
    复制代码
  4. 类的接口

    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属性。

  5. 接口的继承

    和类一样,接口也可以相互继承;利用接口的继承,使用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接口,它同时具有了两个接口的成员和它自己的成员。

进阶用法

  1. 交叉类型(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属性会合并保留一个。

  2. 类型别名

    typescript支持使用type关键字为任何类型创建别名,尤其在使用联合类型时,可以简化代码,如用Code类型表示即可以是number,也可以是string的类型:

    type Code = string | number;
    
    let statusCode: Code = 404;
    statusCode = '503';
    复制代码
  3. 联合类型(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类型。

  4. 类型保护(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);
      }
    }
    复制代码
  5. 可辨识联合(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方法中,可直接使用标签进行判断。

  6. 类型断言

    在某些情况下,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; 
    复制代码

    类型断言只在编译时发挥作用,代码运行过程中不会产生任何影响。

  7. 索引类型

    在typescript中使用索引类型,可以动态的添加和使用任意属性名。如当需要为validation提供一个Error Message的集合:

    interface ErrorContainer {
      [prop: string]: string;
    }
    const errorMsg: ErrorContainer = {
      name: 'Name is required!',
      password: 'Password should be more than 6 digits!'
       // ...... 可任意添加
    }
    复制代码
  8. 函数重载

    函数的重载指的是两个以上的函数,具有相同的函数名,但是形参的个数或者参数的类型不同,编译器可以根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数。

    在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方法报错。

  9. ?.操作符(Nullish Coalescing)

    在typescript 3.7版本中,加入了?.操作符,用C#的同学应该非常熟悉,使用?.可以简化null和undefined的判断:

    const title = userInfo?.job?.title;
    // 相当于
    const title = userInfo && userInfo.job && userInfo.job.title;
    复制代码
  10. ??操作符(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中,同样可以使用范性优化代码。

  1. 什么是范型

    可以将范型看作一种类型变量,它是一种特殊的变量,只用于表示类型而不是值。

    下面定义一个简单的方法,可以接受接受任意类型的参数,并返回参数本身:

    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'
    复制代码
  2. 使用多个范型变量

    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)代码并不会报错。

  3. 范型约束

    在之前定义的merge方法中,可以使用extends关键字为范型变量添加约束;下面的代码将范型变量TU都限制为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'.
    复制代码
  4. 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')将在编码时显示错误。

  5. 范型类

    在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类型进行实例化。

  6. 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求值后必须为一个函数,该函数在运行时被调用,被装饰的声明信息作为参数传入。

  1. 类装饰器(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!
    复制代码
  2. 装饰器工厂(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元素中。

  3. 多个装饰器组合

    在同一个类声明时,可以使用多个装饰器。下面定义了两个装饰器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!
    复制代码
  4. 属性装饰器

    除了类装饰器外,也可为属性添加装饰器,同样,属性装饰器也是在属性声明前调用的;属性装饰器接受两个参数,第一个参数为类的构造函数(静态成员属性)或类的原型对象(实例成员属性);第二个参数为属性名。

    function Logger(target: any, propertyName: string) {
     console.log(target); // Person {}
     console.log(propertyName);// name
    }
    
    class Person {
      @Logger
      name: string;
      constructor() {
        this.name = 'Max';
      }
    }
    复制代码
  5. 访问器装饰器

    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';
      }
    }
    复制代码
  6. 方法装饰器

    方法装饰器在方法的声明前调用,可以用来监视,修改或替换方法的定义。与访问器装饰器一样接收三个参数,分别为:类的构造函数(静态成员)或类的原型对象(实例成员);方法名和成员描述符(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)
      }
    }
    复制代码
  7. 参数装饰器

    除了在方法上使用装饰符外,也可在方法的参数上使用装饰符,参数装饰器声明在一个参数声明之前,应用于类构造函数或方法声明时;参数装饰符接收三个参数,分别为:类的构造函数(静态成员)或类的原型对象(实例成员);方法名和参数在函数参数列表中的索引。下面的示例中,给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)
      }
    }
    复制代码
  8. 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
    ```​		复制代码
分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改