• Typescript 实战 --- (7)类型兼容性 和类型保护


    一、类型兼容性
     
    ts 允许类型兼容的变量相互赋值,这个特性增加了语言的灵活性
     
    当一个 类型Y 可以被赋值给另一个 类型X 时,就可以说类型X兼容类型Y。其中,X被称为“目标类型”,Y被称为“源类型”
    X兼容Y : X(目标类型) = Y(源类型)
    1、结构之间兼容:成员少的兼容成员多的
     
    基本规则是,如果 X 要兼容 Y,那么 Y 至少具有与 X 相同的属性
    interface Named {
      name: string;
    }
    
    let x: Named;
    let y = { name: 'Chirs', age: 23 };
    
    x = y;
    console.log('x', x);   // x { name: 'Chirs', age: 23 }
    
    // 这里要检查 y 是否可以赋值给 x,编译器检查 x 中的每个属性,看能否在 y 中也找到对应的属性
    
    // 相反,把 y 赋值给 x 就会报错,因为 x 不具备 age 属性
    
    y = x;  // Property 'age' is missing in type 'Named' but required in type '{ name: string; age: number; }'
    1-1、子类型赋值
    let s: string = 'hello';
    
    s = null; // 由于在 ts 中, null 是所有类型的子类型,也就是说 字符类型兼容null类型,所以可以赋值
    1-2、接口兼容性
    interface X {
      a: any;
      b: any;
    }
    
    interface Y {
      a: any;
      b: any;
      c: any;
    }
    
    let x: X = { a: 1, b: '2' }
    let y: Y = { a: 3, b: 4, c: 5 }
    
    // 只要源类型y 具备了 目标类型x 的所有属性,就可以认为 x 兼容 y
    x = y;
    console.log('x', x);   // x { a: 3, b: 4, c: 5 }
    2、函数之间兼容:参数多的兼容参数少的
     
    需要判断函数之间是否兼容,常见于两个函数相互赋值的情况下,也就是函数作为参数的情况
     
    2-1、如果要目标函数兼容源函数,需要同时满足三个条件:
     
    (1)、参数个数:目标函数的个数 多余 源函数的个数
    interface Handler {
      (x: number, y: number): void
    }
    
    function foo(handler: Handler) {  // handler:目标函数
      return handler
    }
    
    let h1 = (a: number) => {}  // h1:源函数
    // 目标函数的参数个数2个 > 源函数参数个数1个
    foo(h1);
    
    let h2 = (a: number, b: number, c: number) => {}  // h1:源函数
    // 目标函数的参数个数2个 < 源函数参数个数3个
    foo(h2);  // 类型“(a: number, b: number, c: number) => void”的参数不能赋给类型“Handler”的参数
    (2)、参数类型:参数类型必须要匹配
    interface Handler {
      (x: number, y: number): void
    }
    
    function foo(handler: Handler) {  // handler:目标函数
      return handler
    }
    
    let h3 = (a: string) => {}  // h3:源函数
    
    // 尽管目标函数的参数个数多余源函数的参数个数,但是参数类型不同
    foo(h3); 
    
    /*
      报错信息:
      类型“(a: string) => void”的参数不能赋给类型“Handler”的参数
      参数“a”和“x” 的类型不兼容
      不能将类型“number”分配给类型“string”
    */
    interface Point3D {
      x: number;
      y: number;
      z: number;
    }
    
    interface Point2D {
      x: number;
      y: number;
    }
    
    // 函数 p3d 和 p2d 的参数个数都是1,参数类型都是对象
    let p3d = (point: Point3D) => {}
    let p2d = (point: Point2D) => {}
    
    // 赋值时,依然采用的是目标函数的参数个数必须大于源函数参数个数,且参数类型相同的原则
    p3d = p2d;
    p2d = p3d; // 想要不报错,需要关闭 tsconfig.json 中的一个配置  strictFunctionTypes
    函数的参数之间可以相互赋值的情况,称为 “函数参数双向协变”。它允许把一个精确的类型,赋值给一个不那么精确的类型,这样就不需要把一个不精确的类型断言成一个精确的类型了
     
    (3)、返回值类型:目标函数的返回值类型必须与源函数的返回值类型相同,或为其子类型
    let p = () => ({ name: 'Bob' })
    let s = () => ({ name: 'Bob', age: 23 })
    
    // p 作为目标函数,s 作为源函数时,目标函数的返回值是源函数返回值的子类型
    p = s;
    s = p;  // 不能将类型“() => { name: string; }”分配给类型“() => { name: string; age: number; }”
     
    2-2、关于固定参数、可选参数和剩余参数之间的兼容
     
    1)、固定参数可以兼容可选参数和剩余参数
    2)、可选参数不兼容固定参数和剩余参数
    3)、剩余参数可以兼容固定参数和剩余参数
    // 固定参数
    let a = (x: number, y: number) => {};
    // 可选参数
    let b = (x?: number, y?: number) => {};
    // 剩余参数
    let c = (...args: number[]) => {};
    
    // 固定参数 兼容 可选参数和剩余参数
    a = b;
    a = c;
    
    // 可选参数 不兼容 固定参数和剩余参数 (可将 strictFunctionTypes 设为false 实现兼容)
    b = a;
    b = c;
    
    // 剩余参数 兼容 固定参数和可选参数
    c = a;
    c = b;
    2-3、函数重载
     
    对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名,这样确保了目标函数可以在所有源函数可调用的地方地方
    // 源函数
    function overload(x: number, y: number): number;
    function overload(x: string, y: string): string;
    
    // 目标函数
    function overload(x: any, y: any): any{ };
    // Error1: 目标函数的参数个数 少于 源函数的参数
    // 源函数
    function overload(x: number, y: number): number;  
    // This overload signature is not compatible with its implementation signature
    function overload(x: string, y: string): string;
    
    // 目标函数
    function overload(x: any, y: any, z: any): any{ };
    
    
    // Error2: 目标函数和源函数的返回值类型不兼容
    // 源函数
    function overload(x: number, y: number): number;  
    // This overload signature is not compatible with its implementation signature
    function overload(x: string, y: string): string;
    
    // 目标函数
    function overload(x: any, y: any) { };
    3、枚举类型的兼容性
     
    (1)、枚举类型和数字类型相互兼容
    (2)、枚举类型之间是完全不兼容的
    enum Color { Red, Green, Pink };
    enum Fruit { Apple, Banana, Orange };
    
    // 枚举类型和数字类型相互兼容
    
    let fruit: Fruit.Apple = 4;
    let num: number = Color.Red;
    
    // 相同枚举类型之间不兼容
    let c: Color.Green = Color.Red;
    // 不能将类型“Color.Red”分配给类型“Color.Green”
    
    // 不同枚举类型之间不兼容
    
    let color: Color.Pink = Fruit.Orange;
    // 不能将类型“Fruit.Orange”分配给类型“Color.Pink”
    4、类兼容性
     
    (1)、静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那他们的实例则可以兼容
    class A {
      id: number = 1;
      constructor(p: number, q: number) {}
    }
    
    class B {
      static s: number = 1;
      id: number = 2;
      constructor(p: number) {}
    }
    
    let aa = new A(3, 6);
    let bb = new B(8);
    
    // 两个类都含有相同的实例成员 number 类型的id,尽管构造函数不同,依然相互兼容
    aa = bb;
    bb == aa;
    (2)、如果两个类中含有相同的私有成员,他们的实例不兼容,但是父类和子类的实例可以相互兼容
    class A {
      id: number = 1;
      private name: string = 'hello';
      constructor(p: number, q: number) {}
    }
    
    class B {
      static s: number = 1;
      id: number = 2;
      private name: string = 'hello';
      constructor(p: number) {}
    }
    
    let aa = new A(3, 6);
    let bb = new B(8);
    
    // 在上例的基础上各自添加了相同的 私有成员name,就无法兼容了
    aa = bb;
    bb == aa;
    
    // 均报错:不能将类型“B”分配给类型“A”,类型具有私有属性“name”的单独声明
    class A {
      id: number = 1;
      private name: string = 'hello';
      constructor(p: number, q: number) {}
    }
    
    class SubA extends A {}
    
    let aa = new A(3, 6);
    let child = new SubA(1, 2)
    
    // 就算包含私有成员属性,但是父类和子类的实例可以相互兼容
    aa = child;
    child == aa;
    5、泛型兼容性
     
    (1)、如果两个泛型的定义相同,但是没有指定泛型参数,它们之间也是相互兼容的;
    // demo 1
    interface Empty<T> {};
    
    let a: Empty<string> = {};
    let b: Empty<number> = {};
    
    a = b;
    b = a;
    
    
    // demo 2
    let log1 = <T>(x: T): T => {
      console.log('x');
      return x
    }
    
    let log2 = <U>(y: U): U => {
      console.log('y');
      return y;
    }
    
    log1 = log2;
    (2)、如果泛型中指定了类型参数,会按照结果类型进行比较;
    interface NotEmpty<T> {
      value: T;
    };
    
    let a: NotEmpty<string> = {
      value: 'string'
    };
    let b: NotEmpty<number> = {
      value: 123
    };
    
    a = b; // 不能将类型“NotEmpty<number>”分配给类型“NotEmpty<string>”

    二、类型保护

     
    此处定义了一个枚举Type 和两个类,两个类都有打印的方法,在 getLanguage 函数中,我们希望通过传入不同的参数,调用对应的打印方法
    enum Type { Strong, Weak }
    
    class Java {
      helloJava() {
        console.log('Hello Java')
      }
    }
    
    class JavaScript {
      helloJavaScript() {
        console.log('Hello JavaScript')
      }
    }
    
    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java() : new JavaScript();
    
      // Error:类型“Java | JavaScript”上不存在属性“helloJava”
      if(lang.helloJava) {
        lang.helloJava()        // Error:类型“JavaScript”上不存在属性“helloJava”
      } else {
        lang.helloJavaScript()  // Error:类型“Java”上不存在属性“helloJavaScript”
      }
    
      return lang;
    }
    事实上,在上例中,变量lang被认为是一个联合类型,意味着它必须同时具有 helloJava 和 helloJavaScript 两个方法。此处为了解决报错,就需要借助 类型断言
    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java() : new JavaScript();
    
      // 使用类型断言
      if((lang as Java).helloJava) {
        (lang as Java).helloJava()        
      } else {
        (lang as JavaScript).helloJavaScript()  
      }
    
      return lang;
    }
    
    getLanguage(Type.Strong);    // Hello Java
    由于不知道会传入什么样的参数,因此必须在每一处都加上类型断言。显然,这并不是一个理想的解决方案,代码变得冗长且代码的可读性很差。
     
    类型保护就是用来解决这个问题的,它可以提前对类型进行预判。
     
    1、什么是类型保护
     
    TypeScript 能够在特定的区块中保护变量属于某种确定的类型,可以在此区块中放心的引用此类型的属性,或者调用此类型的方法。
     
    2、创建特定区块的方法:
     
    (1)、instanceOf 判断一个实例是不是属于某个类
    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java() : new JavaScript();
    
      // instanceOf
      if(lang instanceof Java) {
        lang.helloJava()        
      } else {
        lang.helloJavaScript()  
      }
    
      return lang;
    }
    (2)、in 判断一个属性是不是属于某个对象
    enum Type { Strong, Weak }
    
    // 添加一个实例属性,同时要添加构造器,否则在实例对象上还是找不到那个属性
    class Java {
      java: any; 
      constructor(java: any) {
        this.java = java;
      }
    
      helloJava() {
        console.log('Hello Java')
      }
    }
    
    class JavaScript {
      js: any;
      constructor(js: any) {
        this.js = js;
      }
    
      helloJavaScript() {
        console.log('Hello JavaScript')
      }
    }
    
    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java('java') : new JavaScript('js');
    
      // in
      if('java' in lang) {
        lang.helloJava()
      } else {
        lang.helloJavaScript()
      }
    
      return lang;
    }
    
    getLanguage(Type.Strong);    // Hello Java
    (3)、typeof 判断一个变量的类型
    function getLanguage(x: string | number) {
    
      // typeof:此处只是提供一种创建类型保护区块的方法,并不解决此例中的问题
      if(typeof x === 'string') {
        console.log(x.length)
      } else {
        console.log(x.toFixed(2));
      }
    
    }
    (4)、类型保护函数 某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内
    enum Type { Strong, Weak }
    
    class Java {
      helloJava() {
        console.log('Hello Java')
      }
    }
    
    class JavaScript {
      helloJavaScript() {
        console.log('Hello JavaScript')
      }
    }
    
    // 注意类型保护的返回值,是一个“类型谓词”
    function isJava(lang: Java | JavaScript): lang is Java {
      return (lang as Java).helloJava !== undefined
    }
    
    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java() : new JavaScript();
    
      // 类型保护函数
      if(isJava(lang)) {
        lang.helloJava()
      } else {
        lang.helloJavaScript()
      }
    
      return lang;
    }
    
    getLanguage(Type.Strong);    // Hello Java
  • 相关阅读:
    【洛谷P2922】秘密消息【Trie】
    数据结构实验之链表三:链表的逆置
    数据结构实验之链表三:链表的逆置
    数据结构实验之链表一:顺序建立链表
    数据结构实验之链表一:顺序建立链表
    数据结构实验之链表二:逆序建立链表
    数据结构实验之链表二:逆序建立链表
    顺序表应用6:有序顺序表查询
    顺序表应用6:有序顺序表查询
    顺序表应用5:有序顺序表归并
  • 原文地址:https://www.cnblogs.com/rogerwu/p/12205678.html
Copyright © 2020-2023  润新知