Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

盘点 TypeScript 中的易混淆点 #23

Open
gauseen opened this issue Jul 23, 2020 · 0 comments
Open

盘点 TypeScript 中的易混淆点 #23

gauseen opened this issue Jul 23, 2020 · 0 comments
Assignees
Labels
TypeScript TypeScript

Comments

@gauseen
Copy link
Owner

gauseen commented Jul 23, 2020

盘点 TypeScript 中的易混淆点

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

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 gauseen self-assigned this Jul 23, 2020
@gauseen gauseen added the TypeScript TypeScript label Jul 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
TypeScript TypeScript
Projects
None yet
Development

No branches or pull requests

1 participant