TypeScript类型推断,更近一步

842 阅读4分钟

问题

前段时间在 stackoverflow上看到一个提问,提问者在使用window.fetch时遇到了一个问题:

Object literal may only specify known properties, and ''attr'' does not exist in type 'Headers'

最后排查出来的原因可能是attr属性可为undefined

Type '{ 'attr': string | undefined; }' is not assignable to type 'HeadersInit | undefined'.
  Type '{ 'attr': string | undefined; }' is not assignable to type 'undefined'.  TS2322

window.fetch('url', {
  headers: {
       ↑
    attr: attr
  }
})
type HeadersInit = Headers | string[][] | Record<string, string>;

我追到lib.dom.d.ts里面去看Headers的类型是HeadersInit,在本地试了一下,没有出现上述的问题,对象的属性值赋值为undefined编译通过。我想这可能是tsconfig.json里面的lib选项配置有问题,导致用的不是lib.dom.d.ts。虽然本地没有复现,但是问题出现在别人那里,也可以尝试着去解决。

解决方案

对于输入的类型和定义的类型不兼容的情况,TypeScript能给出的解决方案也是不一而足:

使用Headers API

如果内置lib提示不能使用对象字面量headers进行赋值,可以使用Headers构造函数,通过方法调用来设置头部键值对。

const headers = new Headers()
headers.set('attr', attr)
fetch('url', { headers })

直接跳过编译检查

使用注释@ts-ignore@ts-nocheck跳过编译检查,其中@ts-ignore需要添加在需要跳过编译的代码上方,@ts-nocheck需要放在ts文件头部,表示忽略整个文件的编译检查,没有了编译检查,和写js基本没有本质区别,编译时的语法错误将不会得到任何提示。

fetch('url', {
  // @ts-ignore
  headers: {
    attr: attr
  }
})

扩展预设类型

在项目新建global.d.ts声明文件,并在tsconfig.json中将其include进去,可以扩展fetch的第二个参数类型RequestInit或者HeadersTypeScript会将项目中的类型和lib.dom.d.ts中的类型进行自动合并。为什么不能直接扩HeadersInit?因为HeadersInit是个复杂的联合类型,不是用interface声明的类型,无法进行自动合并。

interface Headers {
  [key:string]: string | undefined
}

我们经常会遇到一些需求,需要扩展浏览器window对象nodejs的全局变量,就可以使用类型扩展:

扩展window对象

interface Window {
  // window 对象新增 hello方法
  hello(): void
}

扩展Nodejs环境变量

declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development'| 'production'
    REACT_APP_TITLE: string
  }
}

使用类型断言 as

类型断言是TypeScript中非常人性化的一个功能,因为他把类型推断交到开发者自己的手中。

TypeScript 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」。TypeScript 类型断言用来告诉编译器你比它更了解这个类型,并且它不应该再发出错误。

// solution 1
fetch('url', {
  headers: {
    attr: attr as string
  }
})

// solution 2
fetch('url', {
  headers: {
    attr: attr
  } as HeadersInit
})

// solution 3
fetch('url', {
  headers: {
    attr: attr
  } as Headers
})

类型断言通常是用来做类型收窄操作,但是TypeScript的类型断言可以使用多次,也就是双重断言,能够把一种类型转换成另一种毫不相关的类型,第一次类型断言是扩充类型,第二次则是收窄至想要的类型:

const s = Symbol()
const num = 1
// 下面的语法能够通过编译
const sum = num + (s as unknown as number)
const sum = num + (s as any as number)

使用非空断言 !

非空断言操作符会从变量中移除 undefinednull。这将告诉编译器变量在被调用的时候是已经赋值的状态,从而打消编译器判空的疑虑。

fetch('url', {
  headers: {
    attr: attr!
  }
})

非空断言在一些处理可选属性的链式调用时非常有效,但比类型断言要精简的多!

非空断言!对应的还有可选链?.操作符,可以更优雅的进行判空处理和安全的链式调用。

interface Window{
  a?:{
    b?:{
      c?(): void
    }
  }
}
// 明确告诉编译器可以访问到`c`方法,编译检查通过,但是运行时可能会报错
window.a!.b!.c!()
// 只有`c`方法存在时才会被安全调用
window.a?.b?.c?.()

总结

TypeScript虽然拥有强大的静态类型推断系统,但是依然提供了很多编译选项,类型检查的严格程度由开发者自行决定,开发者也可以使用ts提供的一些黑魔法魔改类型推断,但是在运行时可能会报错,在使用的时候应当谨慎一些,合理的定义类型。