TS:高级类型的十八般武艺

TS:高级类型

1. 交叉类型

  • 交叉类型是将多个类型合并为一个类型,使得这个类型包含了所有类型的特性。

  • 大多数是在混入mixins或者其他不适合典型面向对象模型的地方看到交叉类型的使用。

    function extend<T,U>(first: T,second: U):T & U{
          
          
        let result = <T & U>{
          
          };
        for (let id in first)
            (<any>result)[id] = (<any>first)[id];
        for (let id in second)
            if(!result.hasOwnProperty(id))
                (<any>result)[id] = (<any>second)[id];
    }
    class Person {
          
          
        constructor(public name : string){
          
          }
    }
    interface Loggable {
          
          
        log() : void;
    }
    class Logger implements Loggable{
          
          
        log() {
          
          
            // aaaaaaaaa
        }
    }
    
    let mix = extend(new Person("yivi"),new Logger());
    console.log(mix.name);
    mix.log();
    

2. 联合类型

  • 联合类型作用类似于逻辑操作符——“或”,同样用“|”来分割每个类型。

  • 如果一个值为联合类型,我们只能访问该联合类型的所有类型的公有成员

    interface Bird {
          
          
        fly();
        layEggs();
    }
    interface Fish {
          
          
        swim();
        layEggs();
    }
    
    function getAnimal() : Fish | Bird{
          
          
        // do something
    }
    let pet = getAnimal();
    pet.layEggs();	// ok
    pet.swim();	//error
    
  • 类型保护与区分类型:当我们想要知道联合类型的确切类型是不是Fish时,可以使用类型断言进行判断;

    let pet = getAnimal();
    
    if((<Fish>pet).swim){
          
          
       	(<Fish>pet).swim(); 
    }else{
          
          
        (<Bird>pet).fly();
    }
    

3. 自定义类型保护

  • 所谓类型保护,就是一些表达式,他们会在运行时检查以确保在某个作用域的类型。

  • 定义一个类型保护,我们只要简单地定义一个函数,返回值类型是一个类型谓词

  • 谓词为 parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名。

    function isFish(pet : Fish | Bird) : pet is Fish {
          
          
        return (<Fish>pet).swim != undefined;
    }
    
    // 这样调用就不会有问题了
    
    if(isFish(pet)) {
          
          
        pet.swim();
    }else{
          
          
        pet.fly();
    }
    
  • 实际上,使用typeof来判断更为方便:

    if(typeof pet == Fish) {
          
          
        pet.swim();
    }else{
          
          
        pet.fly();
    }
    
  • instanceof类型保护:这是一种通过构造函数来细化类型的一种方式;

  • instanceof右边要求是一个构造函数,Typescript细化为两种;

    1. 此构造函数的prototype属性的类型(属性类型不为any);

    2. 构造函数所返回的类型的联合;

interface Padder {
    
    
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    
    
    constructor(private numSpaces: number) {
    
     }
    getPaddingString() {
    
    
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    
    
    constructor(private value: string) {
    
     }
    getPaddingString() {
    
    
        return this.value;
    }
}

function getRandomPadder() {
    
    
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    
    
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    
    
    padder; // 类型细化为'StringPadder'
}

4. 可以为null的类型

  • null与undefined可以赋值给任何类型;

  • null与undefined是所有其他类型的一个有效值;

  • --strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含 nullundefined

  • TypeScript会把 nullundefined区别对待。 string | nullstring | undefinedstring | undefined | null是不同的类型。

  • 使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:

    function f(x: number , y?:number){
          
          
        return x + (y || 0);
    }
    f(1, 2);
    f(1);
    f(1, undefined);
    f(1, null); // error, 'null' is not assignable to 'number | undefined'
    
  • 类型断言和类型保护:若编译器不能够去除null或者undefined,可以使用类型断言手动去除。

  • 类型断言语法是添加!后缀:

    function foo(name? : string | null = 'yivi'): string{
          
          
        return name! + ' is a nice guy.';
    }
    

5. 类型别名

  • 类型别名会给一个类型起一个新名字。

  • 类型别名类似于接口,可以作用于原始值,联合类型,元组等;

    type Name = string;
    type Age = number;
    type NameResolver = ()=>string;
    type NameOrResolver = Name | NameResolver;
    
    function getName(n: NameOrResolver): Name {
          
          
        if (typeof n === 'string') {
          
          
            return n;
        }
        else {
          
          
            return n();
        }
    }
    
    // 类型别名也可以是泛型,可以在属性中引用自己
    type Tree<T> = {
          
          
        value : T;
        left : Tree<T>;
        right : Tree<T>;
    }
    
    //联合类型可以类型别名
    type LinkedList<T> = T & {
          
           next: LinkedList<T> };
    
    interface Person {
          
          
        name: string;
    }
    
    var people: LinkedList<Person>;
    var s = people.name;
    var s = people.next.name;
    var s = people.next.next.name;
    var s = people.next.next.next.name;
    
  • 注意:类型别名不能出现在声明类型别名的右侧

    type Foo = Array<Foo>;
    
  • 类型别名与接口孰强孰弱

    1. 接口创建的是一个新的名字,可以在其他任何地方使用,类型别名不创建新名字,所以在报错信息中无法找到;
    2. 在编译器中将鼠标悬停在接口实例上时,会显示接口的具体名称;而类型别名显示的是对象字面量;
    3. 类型别名不能被extends1implements,自己也不能ectendsimplements;
    4. 如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

6. 字符串字面量类型

  • 字符串字面量类型允许指定字符串必须固定的值;

  • 字符串字面量可以很好地与联合类型、类型保护、类型别名很好地配合;

    type Easing = "ease-in" | "ease-out" | "ease-in-out";
    class UIElement {
          
          
        animate(dx : number , dy : number , easing : Easing){
          
          
            if(easing == "ease-in"){
          
          
                //...
            }else if(easing === "ease-out"){
          
          
                //...
            }else iF(easing === "ease-in-out"){
          
          
                //...
            }else{
          
          
                // error
            }
        }
    }
    
  • 字符串字面量类型还可以用于区分函数重载:

    function createElement(tagName: "img"): HTMLImageElement;
    function createElement(tagName: "input"): HTMLInputElement;
    // ... more overloads ...
    function createElement(tagName: string): Element {
          
          
        // ... code goes here ...
    }
    

7. 可辨识联合

  • 可以合并单例类型,联合类型,类型保护,类型别名来创建一个课辨识联合的高级模式,也称作标签联合或者代数数据类型

  • 可识别联合具有三个要素:

    1. 具有普通的单例类型属性
    2. 一个类型的别名包含了哪些类型的联合
    3. 此属性上的类型保护
  • 下面来看看可辩别联合的例子:

    interface Square {
          
          
        kind : "square";
        size : number;
    }
    
    interface Rectangle{
          
          
        kind : "rectangle";
        width : number;
        height : number;
    }
    
    interface Circle {
          
          
        kind : "circle";
        radius : number;
    }
    

    首先我们声明了将要联合的接口。 其中kind属性成为可辨识的特征或者标签

    注意:目前各个接口是没有联系的,下面进行联合:

    type Shape = Square | Circle | Rectangle;
    
    // 可辨识联合
    function area(s : Shape){
          
          
        switch (s.kind){
          
          
            case "square" : return s.size * s.size;
            case "rectangle" : return s.width * s.height;
            case "circle" : return s.radius**2 * Math.PI;
        }
    }
    
  • 完整性检查:当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 TriangleShape,我们同时还需要更新 area

    type Shape = Square | Rectangle | Circle | Triangle;
    function area(s: Shape) {
          
          
        switch (s.kind) {
          
          
            case "square": return s.size * s.size;
            case "rectangle": return s.height * s.width;
            case "circle": return Math.PI * s.radius ** 2;
        }
        // should error here - we didn't handle case "triangle"
    }
    

    解决这个问题的方法,可以定义一个错误函数,使用编译器进行检查:

    function assertNever(x: never): never {
          
          
        throw new Error("Unexpected object: " + x);
    }
    function area(s: Shape) {
          
          
        switch (s.kind) {
          
          
            case "square": return s.size * s.size;
            case "rectangle": return s.height * s.width;
            case "circle": return Math.PI * s.radius ** 2;
            default: return assertNever(s); // error here if there are missing cases
        }
    }
    

    8. 多态的this类型

    • 多态的this类型表示的是某个包含类或接口的子类型,能够容易地表现连贯接口间的继承;

    • 我们用一个计算器的例子来说明:

      class Calculator{
              
              
          constructor (protected value : number = 0){
              
              }
          public currentVal():number{
              
              
              return this.value;
          }
          
          public add(append : number): this{
              
              
              this.value += append;
              return this;
          }
      	public multiply(append : number): this{
              
              
              this.value *= append;
              return this;
          }
      	//...
      }
      
      let c = new Calculator(1);
      c.multiply(2).add(1).currentVal();
      
    • 由于此类使用了this类型,可以直接继承,新的类可以调用之前的方法;

      class ScientificCalculator extends Calculator {
              
              
          constructor(value = 0){
              
              
              super(value);
          }
          public sin(){
              
              
              this.value = Math.sin(this.value);
              return this;
          }
          //...
      }
      
      let v = new ScientificCalculator(2).add(1).sin();
      

    9. 映射类型

    • 有时候,我们想让一个接口里所有的参数都变为可选的或者只读的,我们需要手动去一个一个更改;

      interface PersonPartial {
              
              
          name?: string;
          age?: number;
      }
      
      interface PersonPartial {
              
              
          name?: string;
          age?: number;
      }
      
    • 这样改起来非常的麻烦,所以typescript提供了一种从旧类型创建新类型的方式——映射类型

      type Readonly<T> = {
              
              
          readonly [P in keyof T] : T[P];
      }
      
      type Partitial<T> = {
              
              
          readonly [p in keyof T] : T[P];
      }
      
    • 使用起来非常方便:

      type PersonPartial = Partial<Person>;	// 定义一种类型,将Person的属性全部转化为可选
      type ReadonlyPerson = Readonly<Person>;	// 定义一种类型,将Person的属性全部转化为只读
      
    • 下面来看看最简单的映射例子

      type Keys = 'option1' | 'option2';
      type Flags = {
              
               [K in Keys]: boolean };
      
      // 等同于
      
      type Flags = {
              
              
          option1: boolean;
          option2: boolean;
      }
      
    • 真正的应用场景在于将已有的类型按照一定的方式转换字段:

      type Nullable<T> = {
              
               
          [P in keyof T]: T[P] | null 
      }
      type Partial<T> = {
              
               
          [P in keyof T]?: T[P] 
      }
      type Pick<T, K extends keyof T> = {
              
              
          [P in K]: T[P];
      }
      type Record<K extends string, T> = {
              
              
          [P in K]: T;
      }
      
    • 以上的四种类型应用场景十分广,因此被收进了Typescript标准库里。

猜你喜欢

转载自blog.csdn.net/yivisir/article/details/109827068