TS 类型守卫

发布时间 2023-03-26 16:18:09作者: 小阁下

1. typeof

1.1 typeof 的类型守卫用法

在类型代码中, typeof 执行的是最窄推导程度, 具体如下

const str = '123'

type type1 = typeof str // '123'

通过类型推导, 可以达到收窄变量类型的目的

const foo = (input: string | number) => {
  if (typeof input === 'string') { // input 是 string 类型
    console.log(input.charAt(0))
  } else if (typeof input === 'number') { // input 是 number 类型
    console.log(input.toFixed(2))
  } else {
    throw new Error('type error')
  }
}

foo('123') //=> '1'
foo(233) //=> 233.00

1.2 typeof 的类型守卫函数

关于类型守卫函数, 需要注意的是 is , 在 ReturnType 处填写 oneParam + is + Type , 如下

const isString = (input: unknown): input is string => typeof input === 'string'
const isNumber = (input: unknown): input is number => typeof input === 'number'

const fn = (input: string | number) => {
  if (isString(input)) {
    // input 是 string 类型
    console.log(input.charAt(0))
  } else if (isNumber(input)) {
    // input 是 number 类型
    console.log(input.toFixed(2))
  } else {
    throw new Error('type error')
  }
}

fn('123') //=> '1'
fn(123) //=> 123.00

需要注意的是, 只要你返回的是布尔值, 类型守卫函数就会直接判断是否是 Type 类型, 而不管你这个布尔值是怎么来的, 所以当逻辑与类型不匹配时就很容易出错, 具体如下(这段代码在实际运行时会报错):

const isString = (input: unknown): input is string => typeof input === 'number'
const isNumber = (input: unknown): input is number => typeof input === 'string'

const fn = (input: string | number) => {
  if (isString(input)) {
    // input 其实是 number 类型, 无法调用 charAt 方法
    console.log(input.charAt(0))
  } else if (isNumber(input)) {
    // input 其实是 string 类型, 无法调用 toFixed 方法
    console.log(input.toFixed(2))
  } else {
    throw new Error('type error')
  }
}

fn('123')
fn(233)

1.3 关于 typeof 需要注意的点

需要注意的是, typeof 只有在应用于变量上时会有类型守卫效果, 而应用在变量的属性上时是没有类型守卫的效果的, 如下

interface Foo {
  id: number
  age: number
}

interface Bar {
  id: string
  gender: string
}

const fn = (input: Bar | Foo) => {
  if (typeof input.id === 'string') {
    console.log(input.gender) // 报错, 没有起到类型收窄的作用
  } else if (typeof input.id === 'number') {
    console.log(input.age) // 报错, 没有起到类型收窄的作用
  } else {
    throw new Error('type error')
  }
}

但是这可以通过类型守卫函数解决, 之前也提到过: 类型守卫函数只需要返回布尔值即可帮助进行类型守卫, 至于这个布尔值是怎么来的无所谓. 代码如下:

interface Foo {
  id: number
  age: number
}

interface Bar {
  id: string
  gender: string
}

const isFoo = (input: Bar | Foo): input is Foo => typeof input.id === 'number'
const isBar = (input: Bar | Foo): input is Bar => typeof input.id === 'string'

const fn = (input: Bar | Foo) => {
  if (isBar(input)) {
    console.log(input.gender) 
  } else if (isFoo(input)) {
    console.log(input.age) 
  } else {
    throw new Error('type error')
  }
}

fn({id: 123, age: 234}) //=> 234
fn({id: '123', gender: 'female'}) //=> female

有关属性的类型守卫是于 in 有关的, 此外还有一种方法: 通过对象的字面量进行判断, 如下:

interface Foo {
  id: number
  name: string
  check: 'foo'
}

interface Bar {
  id: string
  gender: string
  check: 'bar'
}

const fn = (input: Bar | Foo) => {
  if (input.check === 'foo') {
    console.log(input.name) // input 类型是 Foo
  } else if (input.check === 'bar') {
    console.log(input.gender) // input 类型是 Bar
  } else {
    throw new Error('type error')
  }
}

fn({id: 1, name: 'saber', check: 'foo'}) //=> saber
fn({id: '1', gender: 'female', check: 'bar'}) //=> female

2. in

2.1 in 的类型守卫用法

当用于判断的属性是预期类型的独有属性时, 就可以实现类型收窄

interface Foo {
  id: number
  name: string
}

interface Bar {
  id: string
  gender: string
}

const f = (input: Bar | Foo) => {
  if ('name' in input) {
    // 由于 'name' 属性是 Foo 类型独有的属性, 故而可以实现类型收窄
    console.log(typeof input.id === 'number')
  } else if ('gender' in input) {
    // 由于 'gender' 属性是 Bar 类型独有的属性, 故而可以实现类型收窄
    console.log(typeof input.id === 'string')
  } else {
    throw new Error('type error')
  }
}

f({id: 123, name: 'saber'}) //=> true
f({id: '123', gender: 'female'}) //=> true

理所当然的, 如果用于判断的属性不是预期类型的独有属性, 则无法进行类型收窄, 如下(这段代码无法通过编译):

interface Foo {
  id: number
  name: string
}

interface Bar {
  id: string
  gender: string
}

const f = (input: Bar | Foo) => {
  if ('id' in input) {
    console.log(typeof input.id === 'number')
  } else {
    // Property 'id' does not exist on type 'never'.
    // 当把具有 id 属性的对象过滤掉后, 就只剩下 never 类型了
    console.log(typeof input.id === 'string')
  }
}

3. instanceof

3.1 instanceof 类型守卫的使用

class FooBase {}

class BarBase {}

class Foo extends FooBase {
  fooOnly() {}
}
class Bar extends BarBase {
  barOnly() {}
}

function handle(input: Foo | Bar) {
  if (input instanceof FooBase) {
    input.fooOnly()
  } else {
    input.barOnly()
  }
}