Hello Typescript(09)-- 类型推断、类型兼容性、类型保护

416 阅读8分钟

类型检查机制

TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。

  • 作用:辅助开发,提高开发效率。
  • 分类:
    • 类型推断
    • 类型兼容性
    • 类型保护

类型推断

不需要指定变量的类型(函数返回值的类型),TypeScript 可以根据某些规则自动推断出一个类型。

  • 基础类型推断

    从右向左推断,根据表达式右侧的值,推断表达式左侧的类型。

    • 变量类型

      let a = 1; // let a: number
      let b = "string"; // let b: string
      
    • 函数默认参数

      let x = (a = 1) => {}; // (parameter) a: number
      
    • 函数返回值

      let x = (a = 1) => a + 1; // let x: (a?: number) => number
      
  • 最佳通用类型推断

    从右向左推断。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。 如果没有找到最佳通用类型的话,类型推断的结果为联合类型。

    let c = [1, 2]; // let c: number[]
    let d = [1, null];
    /*
     ** 在 strictNullChecks:true 时,let d: (number | null)[]
     ** 在 strictNullChecks:false 时,let d: number[]
     */
    
  • 上下文类型推断

    从左向右推断,发生在表达式的类型与所处的位置相关时。

    window.onkeydown = (event: string) => {}; // error!
    window.onkeydown = (event: KeyboardEvent) => {}; // ok!
    
    • 使用场景:
      • 函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。
      • 上下文类型也会做为最佳通用类型的候选类型。
        function createZoo(): Animal[] {
          return [new Rhino(), new Elephant(), new Snake()];
        }
        
        最佳类型候选者有 4 个:AnimalRhinoElephantSnake
  • 如果 ts 的推断不符合你的预期?

    当你有自信比 ts 更了解你的代码时,你可以用 类型断言 来覆盖 ts 的推论。

    let foo = {};
    foo.bar = 1; // Property 'bar' does not exist on type '{}'.
    

    此时,我们可以为 foo 添加一个类型断言。

    interface Foo {
      bar: number;
    }
    let foo = {} as Foo;
    foo.bar = 1; // ok!
    

    但是当我们去掉语句 foo.bar = 1; ,也没有报错;但此时 foo 并没有履行 Foo 的约定。所以,我们还是应该在 foo 声明时直接指定为 Foo 类型。

    let foo: Foo = { bar: 1 };
    

    总结, 类型断言可以增加代码的灵活性,在改造旧代码时非常有效;但不能滥用,不熟悉上下文的情况下,会带来安全隐患。

类型兼容性

TS 允许不同类型的变量在一定规则下相互赋值,增加语言的灵活性。
TS 里的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。它正好与名义(nominal)类型形成对比。

  • 快速记忆

    • 公式化

      当一个 类型 Y 可以被赋值给另一个 类型 X 时,我们就可以说 类型 X 兼容 类型 Y

      X 兼容 Y : X(目标类型) = Y(源类型)

    • 口诀

      结构之间兼容:成员少的兼容成员多的;
      函数之间兼容:参数多的兼容参数少的。

    原始类型和对象类型

    TypeScript 结构化类型系统的基本原则是,如果 x 想兼容 y,那么 y 至少具有和 x 相同的属性。

    // strictNullChecks: false
    let aa = 1;
    let bb = null;
    aa = bb;
    
    let a = { name: "" };
    let b = { name: "string" };
    a = b;
    

    检查函数参数时使用相同的规则。

    interface Named {
      name: string;
    }
    let y = { name: "", age: 1 };
    
    function greet(n: Named) {
      console.log("hello" + n.name);
    }
    
    greet(y);
    

接口

成员少的 兼容 成员多的。

interface X {
  a: number;
  b: number;
}
interface Y {
  a: number;
  b: number;
  c: number;
}
let x: X = { a: 1, b: 2 };
let y: Y = { a: 1, b: 2, c: 3 };
x = y;
y = x; // error!

函数

判断两个函数是否兼容,通常发生在函数相互赋值的情况下。

type Handler = (a: number, b: number) => number;
function hof(handler: Handler) {
  return handler;
}

在这个例子里,handler 作为目标函数,传参作为源函数。 想要 handler 兼容传参,需要满足三个条件。

  1. 参数个数

    目标函数 > 源函数

    let handler1 = (a: number) => number;
    hof(handler1); // ok!
    let handler2 = (a: number, b: number, c: number) => number;
    hof(handler2); // error!
    

    关于 可选参数和剩余参数

    1. 固定参数 兼容 可选参数和剩余参数
    2. 可选参数 不兼容 固定参数和剩余参数
    3. 剩余参数 兼容 固定参数和可选参数
    let a = (a: number, b: number) => number;
    let b = (a?: number, b?: number) => number;
    let c = (...args: number[]) => number;
    
    a = b;
    a = c;
    b = a;
    b = c;
    c = a;
    c = b;
    
  2. 参数类型

    参数类型必须要匹配。

    let handler3 = (a: string) => string;
    hof(handler3); // error!
    

    参数类型复杂的情况(对象类型):将对象属性看成传参,套用参数个数规则,多的兼容少的。

    interface point3D {
      a: number;
      b: number;
      c: number;
    }
    interface point2D {
      a: number;
      b: number;
    }
    let p3d = (value: point3D) => {};
    let p2d = (value: point2D) => {};
    
    p3d = p2d;
    p2d = p3d; // error! strictFunctionTypes: false,可以兼容。
    
    • 函数参数双向协变

      函数参数之间可以互相兼容。

      首先我们需要知道,函数这一类型是逆变的。函数可以看成是一把操作的集合,子类参数 <Dog> 比父类参数 <Animal> 拥有更多的属性/方法。因此 <Animal> 函数是 <Dog> 函数的子集,兼容性是相反的,是逆变。

      那为什么又会有双向协变呢?

      为了维持结构化类型的兼容性,TypeScript 团队做了一个权衡 (trade-off)。保持了函数类型的双向协变性。但是我们可以通过设置编译选项 --strictFunctionTypes: true 来保持函数的逆变性而关闭协变性。

  3. 返回值类型

    同 “ 结构之间兼容:成员少的兼容成员多的。”

    let f = () => ({
      a: number,
    });
    let g = () => ({ a: number, b: number });
    
    f = g; //  OK!
    g = f; // error!
    
    • 函数重载

      函数重载列表重的函数 要兼容 执行函数。

      function overload(a: number, b: number): number; // 目标函数
      function overload(a: string, b: string): string; // 目标函数
      function overload(a: any, b: any): any {} // 源函数
      

枚举类型

枚举默认和数值完全兼容 枚举之间完全不兼容

enum Fruit {
  Apple,
  Banana = "banana",
}
enum Color {
  Red,
  Green,
}
let fruit1: Fruit.Apple = 1; // ok!
let fruit2: Fruit.Banana = 1; // error!
let color1: Color = Color.Red; // ok!
let color2: Color = Fruit.Apple; // error!

类和接口相似,但有一点不同:静态成员(static)和构造函数不在比较的范围内。

class Fruit {
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  name: string;
  size: number = 0;
  age: number;
}
class Clothes {
  static len: number = 100;
  constructor(name: string) {
    this.name = name;
  }
  name: string;
  size: number = 0;
}
let f = new Fruit("apple", 12);
let c = new Clothes("t-shirt");

f = c; // error!
c = f; // ok!

类中有私有成员(private),只能父类兼容子类(或相互兼容)。

class Fruit {
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  name: string;
  private size: number = 0;
}
class Apple extends Fruit {}
let a = new Apple("apple", 1);
f = a; // OK!

泛型

interface Empty<T> {}
let a: Empty<number>;
let b: Empty<string>;

a = b!; // OK, because y matches structure of x

上面代码里,xy 是兼容的,因为它们的结构使用类型参数时并没有什么不同。

interface NotEmpty<T> {
  data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y; // error!

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成 any 比较。 然后用结果类型进行比较,比如。

let identity = function <T>(x: T): T {
  return x;
};

let reverse = function <U>(y: U): U {
  return y;
};

identity = reverse; // OK, because (x: any) => any matches (y: any) => any

类型保护

类型保护允许你使用更小范围下的对象类型。

  • 现在有这样一个 demo👇

    enum Type {
      Strong,
      Week,
    }
    
    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();
      return lang;
    }
    
    getLanguage(Type.Strong);
    getLanguage(Type.Week);
    

    现在,我们想在 getLanguage 里调用 Java/JavaScript 的 hello 方法。

    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java() : new JavaScript();
      if (lang.helloJava) {
        // error!
        lang.helloJava(); // error!
      } else {
        lang.helloJavaScript(); // error!
      }
      return lang;
    }
    

    Property 'helloJava'/'helloJavaScript' does not exist on type 'Java | JavaScript'. 当然,我们可以通过 类型断言 来去掉错误。

    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;
    }
    

    但是我们要在每一处都添加类型断言,这显然不是合理的解决方案;

    于是,我们就要启动 类型保护体制 了。

    ts 能够在特定的区块中保证变量属于某种确定的类型;在此区块中,可以泛型使用此类型的属性,调用此类型的方法。

  • 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;
    }
    
  • in

    判断一个属性是否属于某个对象

    function getLanguage(type: Type) {
      let lang = type === Type.Strong ? new Java() : new JavaScript();
      // in
      if ("helloJava" in lang) {
        lang.helloJava();
      } else {
        lang.helloJavaScript();
      }
      return lang;
    }
    
  • typeof

    判断一个变量的类型

    function doSome(x: number | string) {
      if (typeof x === "string") {
        x.length;
      } else {
        x++;
      }
      return x;
    }
    
  • 保护函数

    某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内

    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;
    }
    

TypeScript 基础系列