【Typescript小手册】类型兼容

221 阅读6分钟

概述

Typescript 的类型兼容指不同的类型的变量可以相互替换和给编译器额外信息来识别具体类型,主要包括:

  • 变量类型兼容
  • 函数类型兼容
  • 类型守卫

兼容

变量类型兼容

Typescript 的类型兼容不是依据类型名称,而是类型的成员。也就是说,两个类型如果成员相同或是其中一个类型只缺少可选类型,那么这两个类型就是兼容的。比如:

interface A {
  name: string
  value?: number
}
interface B {
  name: string
}

let a: A = { name: 'b' } as B
let c: stirng = 0 // 错误

因为类型AB的必需属性都是name,所以B类型变量可以赋值给A类型变量。但是stringnumber不兼容,无法互相赋值。

在看一个例子:

function log(x: { name: string }) {}

let a = {
  name: 'a',
  value: 0
}

log(a)

参数x要求对象有一个string类型的name成员,虽然对象a多了一个value成员,但是只要它有string类型的name成员,就满足兼容条件。

但有一个例外情况,如果给函数传入一个直接量,那么必须与参数的对象成员一致,不能有多余的成员,返回值同样如此。比如:

log({ name: 'a', value: 0 }) // 错误。value多余

类实例

不同类实例之间,只要实例成员兼容则类型兼容,不要求静态成员兼容。

示例:

class A {
  static value: string = 'a'

  log(x: string): void {}
}
class B {
  static value: number = 0

  log(x: string): void {}
}

let a: A = new B()

函数类型兼容

函数类型兼容主要是指函数的参数数量类型和返回值类型的兼容。

示例:

type Scale = (x: string, y: string) => { length: number }

let scale: Scale = function (x: string): { length: number } {
  let result = { length: x.length }

  return result
}

let v = scale('x', 'y')

函数参数和返回值也遵循之前的变量类型的兼容,除此外它还有更宽泛的兼容性,且参数和返回值的兼容方向不同。示例:

type Scale = (x: string, y: string) => { length: number }

let scale: Scale = function (x: string): { length: number } {
  let result = { length: x.length }

  return result
}

let v = scale('x', 'y')

Scale类型函数的参数有两个,但是变量scale被赋予只有一个参数的函数值,但这是合法的。参数是从函数外流向函数内,Scale类型对外要求两个参数,但是这两个参数在函数体内并不一定需要全部被用到。因为函数体即使只使用了一个参数,另一个参数没有使用,也不会发生运行时错误,所以这样的类型是兼容的。也就是说,函数变量的参数只要类型兼容且数量小于等于类型声明的参数数量,两者就是兼容的。

再来看返回值类型不同的情况:

type Scale = (x: string) => { length: number }

let scale: Scale = function (x: string): { length: number } {
  let result = { length: x.length, value: x }

  return result
}

let v = scale('x')

Scale类型函数的返回值只有一个length成员,但是变量scale的返回值有lengthvalue两个成员,这也是合法的。返回值从函数内流向函数外,Scale类型对外宣称返回的对象有一个成员,那么只要在满足外部期望,即返回了length成员的情况下,就是合法的。置于额外的成员是否被用到,是编译器无法推断的。

总结来说,就是数据流出方满足数据流入方的期望,不会造成出现undefined的运行时错误,类型就是兼容的。

类型守卫

null 和 undefined

有些变量用联合类型使得它们可以为空(nullundefined),比如:

function scale(x: string | null) {
  return x.length // 错误
}

因为x可能为null,但是null没有length成员,所以编译器报错。可以通过判断x是否存在来兼容:

function scale(x: string | null) {
  if (x) {
    // 或 x !== null
    return x.length
  } else {
    return 0
  }
}

有时我们可以确认一些可控类型的变量一定有值,那么我们可以使用操作符!来告诉编译器这个变量不可能为nullundefined。比如:

let a = document.getElementById('a')

let b = a.tagName // 错误
let c = a!.tagName

因为变量a可能为null所以不能直接读取tagName属性,如果我们能确定这个元素存在,我们可以在变量a后加上!,编译器就认为a一定有值,不再报错。

多类型函数参数

有些函数的参数类型是一个联合类型,可以传入多种类型的参数,但在使用参数时不一定兼容。

function scale(x: string | number) {
  return x.length // 错误
}

因为number类型没有length成员,所以 Typescript 会报告这里可能会有错误。我们可以使用typeof来判断这里的类型,比如:

function scale(x: string | number) {
  if (typeof x === 'string') {
    return x.length
  } else {
    return x.toString().length
  }
}

Typescript 编译器会检测到代码判断了类型,所以这里读取length属性是安全的。

也可以使用instanceof判断原型。示例:

function value(x: Date | string) {
  if (x instanceof Date) {
    return x.toUTCString()
  } else {
    return x.toUpperCase()
  }
}

成员名

通过操作符in判断成员是否存在,进而判断类型。

示例:

interface A {
  name: string
}
interface B {
  value: number
}

function scale(x: A | B) {
  if ('name' in x) {
    return x.name.length
  } else {
    return 0
  }
}

接口字面量成员

对于有字面量类型成员的类型,可以通过判断字面量成员来确定类型。

示例:

interface A {
  name: 'a'
  value: string
}
interface B {
  name: 'b'
  value: number
}

function scale(x: A | B) {
  if (x.name === 'a') {
    return x.value.length
  } else {
    return 0
  }
}

因为接口A的成员name只能被赋值为a,所以在函数中如果判断参数x的成员name === 'a',就能推断xA类型,那么其value属性是字符串,可以读取length成员。

自定义类型守卫

如果上述的方法不够用,我们可以自定义类型守卫,就是编写一个判断函数。示例:

interface A {
  name: 'a'
  value: string
}
interface B {
  name: 'b'
  value: number
}

function isA(x: any): x is A {
  return x.name === 'a'
}

function scale(x: A | B) {
  if (isA(x)) {
    return x.value.length
  } else {
    return 0
  }
}

我们通过在函数isA的返回值处用操作符is来声明这是一个自定义的类型守卫,且这个守卫的用处就是用来判断参数x是否是A类型。函数体内是进行判断的具体逻辑,函数的返回值必须是boolean。这个函数的意义就是由我们来告诉编译器怎么判断一个参数是否是A类型,可以用在判断语句(比如if)中,这样编译器对相应的变量可以进行类型推断。

在类中可以使用this作为is的操作数。示例:

class C implements A {
  name: 'a' = 'a'
  value: string = '0'

  isA(): this is A {
    return this.name === 'a'
  }
}