# 类型检查机制

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

作用:辅助开发,提高开发效率。

  • 类型推断
  • 类型兼容性
  • 类型保护

# 类型推断

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

  • 基础类型推断
  • 最佳通用类型推断
  • 上下文类型推断

基础类型推断,最佳通用类型推断 都是从右往左的推断,根据表达式右侧的值来推测表达式左侧变量的类型。

上下文类型推断是从左往右的推断,通常出现在 事件处理 中。

类型推断不符合你的要求的时候,你可以使用 类型断言 as

类型断言 可以增加代码的灵活性,在改造旧代码时非常有效,但是类型断言要 避免滥用 ,要对自己 上下文 充足的 预判没有任何根据的类型断言,会给你的代码 安全隐患

# 基础类型推断

这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

// 初始化变量 x:number
let x = 3

// 设置默认参数值 x:number
let y = (x=1)=>{}

// 确定函数返回值 z:number
let z = (x = 1) => { x + 1}

# 最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,

let x = [0, 1, null]

为了推断 x 的类型,我们必须考虑所有元素的类型。 这里有两种选择:number 和 null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

由于最终的通用类型取自候选类型,有些时候候选类型共享一个公共结构,但是却没有一个类型能做为所有候选类型的超级类型。例如:

class Animal {
  numLegs: number
}

class Dog extends Animal {
}

class Lion extends Animal {
}

let zoo = [new Dog(), new Lion()]

这里,我们想让 zoo 被推断为 Animal[] 类型,但是这个数组里没有对象是 Animal 类型的,因此不能推断出这个结果。 为了更正,我们可以明确的声明我们期望的类型:

let zoo: Animal[] = [new Dog(), new Lion()]

如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Dog | Lion)[]

# 上下文类型推断

有些时候,TypeScript 类型推断会按另外一种方式,我们称作 上下文类型;上下文类型的出现和表达式的类型以及所处的位置相关。通常出现在 事件处理 中,比如:

window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.clickTime)  // Error
}

这个例子会得到一个类型错误,TypeScript 类型检查器使用 window.onmousedown 函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent 参数的类型了,所以 mouseEvent 访问了一个不存在的属性,就报错了。

如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。重写上面的例子:

window.onmousedown = function(mouseEvent:any) {
  console.log(mouseEvent.clickTime)  // OK
}

这个函数表达式有明确的参数类型注解,上下文类型被忽略。这样的话就不报错了,因为这里不会使用到上下文类型。

上下文类型会在很多情况下使用到。通常包含函数的参数,赋值表达式的右边,类型断言,对象成员,数组字面量和返回值语句。上下文类型也会做为最佳通用类型的候选类型。比如:

function createZoo(): Animal[] {
  return [new Bee(), new Lion()]
}

let zoo = createZoo()

这个例子里,最佳通用类型有 3 个候选者:Animal,Bee 和 Lion。 其中,Animal 会被做为最佳通用类型。

# 类型兼容性

当一个 类型Y(源类型)可以被赋值给另一个 类型X(目标类型) 时,可以说类型X兼容类型Y。

源类型必须具备目标类型的 必要属性

口诀:

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

TypeScript允许在类型兼容的变量之间相互赋值,这个特性增加了语言的 灵活性

# 接口兼容性

接口之间相互兼容时,成员少的可以兼容成员多的。

// 接口兼容性
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: 1, b: 2, c: 3 }
x = y
// Property 'c' is missing in type 'X' but required in type 'Y'.
y = x

# 函数兼容性

判断两个函数是不是兼容,通常发生在两个函数相互赋值的情况下,也就是函数作为参数的情况下。当给下面的高阶函数 hof 传递 参数 时,就会判断 参数Handler 是不是类型兼容。Handler 是目标类型参数 是源类型

要目标类型,兼容源类型需要满足3个条件:

  • 1)参数个数
    • 目标函数的参数个数要多于源函数的参数个数
    • 固定参数可以兼容剩余参数,可选参数
    • 可选参数不兼容固定参数 ,剩余参数。可以设置 strictFunctionTypes:false 实现兼容
    • 剩余参数兼容固定参数,可选参数
  • 2)参数类型
    • 目标函数的参数类型要包含源函数的参数类型
  • 3)函数返回值
    • 成员少的可以兼容多的
// 函数兼容性
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
  return handler
}

// 1)参数个数
let handler1 = (a: number) => { }
hof(handler1)
let handler2 = (a: number, b: number, c: number) => { }
// 类型“(a: number, b: number, c: number) => void”的参数不能赋给类型“Handler”的参数。
// hof(handler2)

// 可选参数和剩余参数
let a = (p1: number, p2: number) => { }
let b = (p1?: number, p2?: number) => { }
let c = (...args: number[]) => { }
a = b
a = c
// 可选参数不兼容固定参数 ,剩余参数
// b = a
// b = c
c = a
c = b

// 2)参数类型
let handler3 = (a: string) => { }
// hof(handler3)

interface Point3D {
  x: number;
  y: number;
  z: number;
}
interface Point2D {
  x: number;
  y: number;
}
let p3d = (point: Point3D) => { }
let p2d = (point: Point2D) => { }
p3d = p2d
// 成员少的可以兼容多的
// p2d = p3d

// 3) 返回值类型
let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'Alice', location: 'Beijing' })
f = g
// 不能将类型“() => { name: string; }”分配给类型“() => { name: string; location: string; }”。Property 'location' is missing in type '{ name: string; }' but required in type '{ name: string; location: string; }'.
// g = f

# 函数重载兼容性

函数重载包括两个部分,一部分是重载列表,一部分是具体实现。列表中的函数是目标函数具体实现是源函数。程序运行时,编译器会查找重载列表,使用第一个匹配的定义,来执行源函数。

在函数重载中,目标函数参数个数多于或者等于源函数参数个数。并且返回值也要兼容目标函数返回值。

// 函数重载 
// 重载列表
function overload(a: number, b: number): number
function overload(a: string, b: string): string
// 具体实现
function overload(a: any, b: any): any { }
// function overload(a: any): any {}
// This overload signature is not compatible with its implementation signature.
// function overload(a: any, b: any, c: any): any {}
// This overload signature is not compatible with its implementation signature.
// function overload(a: any, b: any) {}

# 枚举类型兼容性

枚举类型和数字类型是完全兼容的。枚举之间是完全不兼容的。

// 枚举兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
let fruit: Fruit.Apple = 1
let no: number = Fruit.Apple
// 不能将类型“Fruit.Apple”分配给类型“Color.Red”。
let color: Color.Red = Fruit.Apple

# 类兼容性

比较两个类的兼容性时,类的构造函数和静态成员不参与比较。

如果两个类具有相同的实例成员,它们的实例就可以完全相互兼容。

当类中有私有成员时,这两个类就不相互兼容了,只有父类和子类之间可以相互兼容。

// 类兼容性
class A {
  constructor(p: number, q: number) { }
  id: number = 1
  private name: string = ''
}
class B {
  static s = 1
  constructor(p: number) { }
  id: number = 2
  private name: string = ''
}
class C extends A { }
let aa = new A(1, 2)
let bb = new B(1)
// 不能将类型“B”分配给类型“A”。类型具有私有属性“name”的单独声明。
aa = bb
// 不能将类型“A”分配给类型“B”。类型具有私有属性“name”的单独声明。
bb = aa
let cc = new C(1, 2)
aa = cc
cc = aa

# 泛型兼容性

泛型接口中,只有泛型变量被接口成员使用才影响泛型的兼容性

// 泛型接口
interface Empty<T> {
  value: T
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
// 不能将类型“Empty<string>”分配给类型“Empty<number>”。不能将类型“string”分配给类型“number”。
obj1 = obj2

上面如果 泛型变量 T 如果没有被成员变量使用则兼容

// 泛型接口
interface Empty<T> {
  // value: T //没有使用泛型变量 T
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
obj1 = obj2

如果两个泛型函数的定义相同,但是没有指定类型参数,那么它们之间也是可以互相兼容的。

let log1 = <T>(x: T): T => {
  console.log('x')
  return x
}
let log2 = <U>(y: U): U => {
  console.log('y')
  return y
}
log1 = log2

# 类型保护

TypeScript能够在特定的区块中保证变量属于某种确定的类型。

可以在此区块中引用此类型的属性,或者调用此类型的方法。

问题引入

enum Type { Strong, Week }

class JavaScript {
  helloJavaScript() {
    console.log('Hello JavaScript')
  }
  js: any
}

function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java() : new JavaScript();

  // 类型“Java | JavaScript”上不存在属性“helloJava”。类型“JavaScript”上不存在属性“helloJava”。
  // if (lang.helloJava) {
  //   lang.helloJava();
  // } else {
  //   // 类型“Java | JavaScript”上不存在属性“helloJavaScript”。类型“Java”上不存在属性“helloJavaScript”。
  //   lang.helloJavaScript();
  // }
  // 类型断言
  if ((lang as Java).helloJava){
    (lang as Java).helloJava();
  }else{
    (lang as JavaScript).helloJavaScript();
  }
}

getLanguage(Type.Week)

上面的代码使用类型断言,代码可读性很差,使用 类型保护 机制可以解决这个问题,可以提前预判变量的类型。

可以通过下面的方法解决:

  • instanceof:判断实例是不是属于某个类
  • in:判断一个属性是否属于某个对象
  • typeof:判断一个变量的类型
  • 类型保护函数:某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内
enum Type { Strong, Week }

class Java {
    helloJava() {
        console.log('Hello Java')
    }
    java: any
}

class JavaScript {
    helloJavaScript() {
        console.log('Hello JavaScript')
    }
    js: any
}

// 类型保护函数
// lang is Java:类型谓词
function isJava(lang: Java | JavaScript): lang is Java {
    return (lang as Java).helloJava !== undefined
}

function getLanguage(type: Type, x: string | number) {
    let lang = type === Type.Strong ? new Java() : new JavaScript();
    
    if (isJava(lang)) {
        lang.helloJava();
    } else {
        lang.helloJavaScript();
    }

    // if ((lang as Java).helloJava) {
    //     (lang as Java).helloJava();
    // } else {
    //     (lang as JavaScript).helloJavaScript();
    // }

    // instanceof
    // if (lang instanceof Java) {
    //     lang.helloJava()
    //     // lang.helloJavaScript()
    // } else {
    //     lang.helloJavaScript()
    // }

    // in
    // if ('java' in lang) {
    //     lang.helloJava()
    // } else {
    //     lang.helloJavaScript()
    // }

    // typeof
    // if (typeof x === 'string') {
    //     console.log(x.length)
    // } else {
    //     console.log(x.toFixed(2))
    // }

    return lang;
}

getLanguage(Type.Week, 1)

# 参考

更新时间: 5/13/2020, 11:23:00 PM