盘点 TypeScript 中的易混淆点

本文所有示例均可在 Playground 验证 😄

原文:https://github.com/gauseen/bl...

any VS unknown VS void VS never

any

any 用来表示可以赋值为任意类型,包括 any 类型值的属性和方法,所有类型都能被赋值给它,它也能被赋值给其他任何类型,在 TypeScript 中尽量避免使用

let anyThing: any = 'hello'

// 以下在编译时不会报错,在运行时报错,失去了 TypeScript 类型检查的意义
console.log(anyThing.todo())
console.log(anyThing.todo().abc)

unknown

unknownany 类型对应的安全类型,在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查

let value: unknown

value = undefined // ok
value = null // ok
value = true // ok
value = 86 // ok
value = 'hello' // ok
value = {} // ok
value = Symbol() // ok

unknown 类型大多数操作都认为是错误的

let value: unknown

let v1: unknown = value // ok
let v2: any = value // ok
let v3: undefined = value // Error
let v4: null = value // Error
let v5: string = value // Error
let v6: number = value // Error
let v7: boolean = value // Error
let v8: symbol = value // Error

console.log(value.key) // Error
value.foo() // Error

所以在操作 unknown 类型前,应该缩小类型范围,可以通过:typeof、instanceof、as、is

let value: unknown = 'hello'

// 通过 typeof 缩小类型范围
if (typeof value === 'string') {
  console.log(value.length)
}

void

void 表示没有任何类型,只能将它赋值为 undefinednull

// 用 void 表示没有任何返回值的函数
function alertFunc(): void {
  alert('gauseen')
}

never

never 表示永远不存在的值的类型, never 类型只能赋值给另外一个 never

// 一个从来不会有返回值的函数
function foo(): never {
  while (true) {}
  alert('执行?')
}

// 一个总是会抛出错误的函数
function foo(): never {
  throw new Error('some error')
}

当一个函数没有返回值时,它返回了一个 void 类型,但是,当一个函数根本就没有返回值时(陷入死循环或者总是抛出错误),它返回一个 nevervoid 指可以被赋值的类型(在 strictNullChecking 为 false 时),其他任何类型不能赋值给 never,除了 never 本身以外

interface VS type

开发中,经常用 interfacetype 用于类型声明,对与新手来说非常容易混淆,下面梳理一下它们之间的相同点与不同点。

相同点

定义对象或函数

两者都可以定义对象或者函数,但是语法有所不同,示例如下:

// interface

interface Point {
  x: number
  y: number
}

interface SetPoint {
  (x: number, y: number): void
}
// type

type Point = {
  x: number
  y: number
}

type SetPoint = (x: number, y: number) => void

Extend(继承)

两者都可以 extends(继承),但语法不同。包括,interface 可以继承 typetype 也可以继承 interface

⚠️ interface 不可以继承 type 的联合类型

// interface extends interface
interface PointX {
  x: number
}
interface Point extends PointX {
  y: number
}

// type alias extends type alias
type PointX = { x: number }
type Point = PointX & { y: number }

// interface extends type alias
type PointX = { x: number }
interface Point extends PointX {
  y: number
}

// Error: interface 无法继承联合类型
type PointX = { x: number } | { y: number }
interface Point extends PointX {
  y: number
}

// type alias extends interface
interface PointX {
  x: number
}
type Point = PointX & { y: number }

Implements(实现)

一个 class 可以实现 interface 或 type

⚠️ class 不能实现 type 的联合类型

interface Point {
  x: number
  y: number
}

class SomePoint implements Point {
  x = 1
  y = 2
}

type Point2 = {
  x: number
  y: number
}

class SomePoint2 implements Point2 {
  x = 1
  y = 2
}

type Point3 = { x: number } | { y: number }

// Error: 无法实现联合类型
class SomePoint3 implements Point3 {
  x = 1
  y = 2
}

不同点

type 用于其他类型的别名

type 也可以用于其他类型,如:基本类型、联合类型、元组,但 interface 不可以

// 基础类型的别名
type Name = string

// 联合类型(union)
type Age = string | number

// 元组
type Data = [number, string]

声明合并

一个 interface 可以定义多次,并将做为单个接口(所有声明的成员都将被合并),但 type 不可以同名多次声明

interface Point {
  x: number
}
interface Point {
  y: number
}

const point: Point = { x: 1, y: 2 }

怎么使用?

对于如何使用,其实这是个仁者见仁智者见智的问题,下面说一下我的看法
  • 前置条件,使用时首先应该与团队已有规范保持一致
  • 平常使用时,建议尽量使用 interface 来代替 type,官方文档也有所说明
  • 无法通过 interface 来定义一个类型时,选择使用 type,例如描述,基础类型的别名、联合类型、元组

is VS as

is

TypeScript 中 is 关键字表示是否属于某个类型,可以有效地缩小类型范围

如下代码,封装一个 isString 函数,来判断某个值是否为 string 类型,函数返回值为 boolean 类型。

function isString(val: any): boolean {
  return typeof val === 'string'
}

function example(foo: any) {
  if (isString(foo)) {
    console.log('a string' + foo)
    console.log(foo.length)
    console.log(foo.toSome(2)) // 编译不报错,在运行时报错,foo 没有 toSome 方法
  }
}

上面的代码编译阶段不报错,但在运行时会报错,但是为什么 TS 类型检测没有报错呢?默认这种情况下 TypeScript 不会缩小块作用域中的类型,此时 TypeScript 认为 fooany 类型,所以 foo.toSome(2) 不会出现编译错误,但 foo.toSome() 方法确实不存在,所以会出现运行时错误。那么如何避免这种情况发生呢?—— 使用 is 关键字,具体示例如下:

function isString(val: any): val is string {
  return typeof val === 'string'
}

function example(foo: any) {
  if (isString(foo)) {
    console.log('it is a string' + foo)
    console.log(foo.length)
    console.log(foo.toSome(2)) // 编译时报错,运行时报错
  }
}

使用 val is string 函数返回类型,而不是将 boolean 用为函数返回类型。因为在调用 isString() 之后,如果函数返回 true,TypeScript 会将类型范围缩小为 string,在编译时就能发现代码错误。

is 关键字可以有效的缩小类型范围,可以帮助我们在编辑阶段发现错误,从而避免一些隐藏的运行时错误,这也是 TypeScript 的优势所在。

as

TypeScript 允许手动覆盖它的推断,可以手动指定某个值的类型,这种机制被称为「类型断言」。

可断言的情况:

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
  • 要使得 A 能够被断言为 B,只需要 A 兼容 BB 兼容 A 即可
interface IPerson {
  age: number
  name: string
  weight: string
}

const gauseen: IPerson = {
  age: 26,
  name: 'gauseen',
  weight: '600kg',
}

// Error
// 因为 `IPerson` 类型的索引值只有 `"age" | "name" | "weight"`,所以 `string` 类型不能作为 `IPerson` 类型的索引
function getValue(obj: IPerson, key: string) {
  return obj[key]
}

getValue(gauseen, 'age')

// 注⚠️:只是做演示使用,不推荐这样断言
function getValue(obj: IPerson, key: string) {
  return obj[key as keyof IPerson]
}

getValue(gauseen, 'age')

// OK
function getValue(obj: IPerson, key: keyof IPerson) {
  return obj[key]
}

getValue(gauseen, 'age')

keyof

keyofinterface 的键,返回值可作为一个联合类型

interface IPerson {
  age: number
  name: string
  weight: string
}

const gauseen: IPerson = {
  age: 26,
  name: 'gauseen',
  weight: '65kg',
}

// Error
Object.keys(gauseen).map((key) => gauseen[key])
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'IPerson'.
// No index signature with a parameter of type 'string' was found on type 'IPerson'.(7053)

为什么第一个会报错?
因为 Object.keys() 返回 string[],string 类型值不能作为 gauseen 对象的索引,因为取值有可能会返回 undefined

如何解决?
应该给 Object.keys(gauseen) 返回值做个约束/断言,让 ts 知道它的返回的值属于 gauseen 对象中的某个键,如下:

// Pass
type Keys = 'age' | 'name' | 'weight'
;(Object.keys(gauseen) as Array<Keys>).map((key) => gauseen[key])

通过 keyof 更优雅的控制,实际效果跟上面一样

// Ok
;(Object.keys(gauseen) as Array<keyof IPerson>).map((key) => gauseen[key])

最后

欢迎关注无广告文章公众号:学前端

参考


gauseen
1.2k 声望12 粉丝