learn-typescript

高级类型

交叉类型

交叉类型( Intersection Types )是将多个类型合并成一个类型,使用 & 符号连接多个类型,包含所有类型的特性。

function extend<T, U>(first: T, second: U): T & U {
  let ret = {} as any

  for (let key in first) {
    ret[key] = first[key]
  }

  for (let key in second) {
    if (!ret.hasOwnProperty(key)) {
      ret[key] = second[key]
    }
  }

  return <T & U>ret
}

class Square {
  name: string
  length: number
  constructor(length: number) {
    this.name = 'square'
    this.length = length
  }
}

class Circle {
  name: string
  pi: number
  radius: number
  constructor(radius: number) {
    this.name = 'circle'
    this.pi = 3.14
    this.radius = radius
  }
}

let squ = new Square(1)
let cir = new Circle(2)
let rectangle = extend(squ, cir)
console.log(rectangle) //{ name: 'square', length: 1, pi: 3.14, radius: 2 }

联合类型

联合类型(Union Types)表示一个值可以是几种类型之一,使用 | 符号连接多个类型。

function padLeft(str: string, padding: string | number) {
  if (typeof padding === 'string') {
    return padding + str
  } else {
    return new Array(padding + 1).join(' ') + str
  }
}

console.log(padLeft('abc', 4))
console.log(padLeft('abc', 'cba'))

类型保护

联合类型允许值为多种类型之一,但是该值只能调用多种类型所共有的成员,因为没有确定该值究竟是哪种类型。

interface Bird {
  eat(): void
  fly(): void
}

interface Fish {
  eat(): void
  swim(): void
}

function getRandomPet(): Bird | Fish {
  let eat = function(): void {
    console.log('eat')
  }
  let fly = function(): void {
    console.log('fly')
  }
  let swim = function(): void {
    console.log('swim')
  }
  let flag = Math.random() > 0.5
  if (flag) {
    let bird: Bird = { eat, fly }
    return bird
  } else {
    let fish: Fish = { eat, swim }
    return fish
  }
}

let pet: Bird | Fish = getRandomPet()

pet.eat()  // ok
pet.swim()  // 错误,swim方法不是公共的成员

如上面代码所示,pet 只能调用 eat() 方法,如果需要确定 pet 为哪种具体类型,从而调用该类型特有的方法,需要使用 类型断言

if((pet as Fish).swim) {
  ;(pet as Fish).swim()
}else {
  ;(pet as Bird).fly()
}

用户自定义的类型保护

注意到为了确定 pet 的具体类型,调用该类型特有的方法,需要多次使用 类型断言,非常麻烦。
TypeScript 提供了 类型保护 机制来避免多次使用 类型断言一旦检查过变量的类型之后,可以确定某个作用域中该变量的类型
要定义一个 类型保护,需要定义一个函数,它的返回值为一个 类型谓词

function isFish(pet: Bird | Fish): pet is Fish {
  return (pet as Fish).swim !== undefined
}

这个例子中 pet is Fish 就是 类型谓词。 谓词的形式为: parameterName is Type,其中 parameterName 是当前函数的一个参数。
每当调用 isFish() 之后,TypeScript 会将传入的变量缩减为具体的类型:

if(isFish(pet)){
  pet.swim()
}else {
  pet.fly()  // TypeScript还能确定else分支中是Bird类型
}

typeof类型保护

对于原始类型 number , string, boolean, symbol,不需要写一个函数来实现类型保护,可以直接使用 typeof 来实现 类型保护

注意:
对于 undefinednull ,直接使用 arg === undefinedarg === null 即可实现类型保护;
typeof null 的值为 object

function padLeft(str: string, padding: string | number) {
  if (typeof padding === 'string') {
    return padding + str
  }
  if( typeof padding === 'number'){
    return new Array(padding + 1).join(' ') + str
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

instanceof类型保护

instanceof类型保护类似 typeof ,它是通过构造函数来实现细化类型的。

class Dog {
  name: string
  bark: () => void
  constructor() {
    this.name = 'dog'
    this.bark = function() {
      console.log('wang wang')
    }
  }
}

class Cat {
  name: string
  climb: () => void
  constructor() {
    this.name = 'cat'
    this.climb = function() {
      console.log('climb')
    }
  }
}

function getPet(): Dog | Cat {
  if (Math.random() < 0.5) {
    return new Dog()
  } else {
    return new Cat()
  }
}
let pet: Dog | Cat = getPet()

if (pet instanceof Dog) {
  pet.bark()
} else {
  pet.climb()
}

null和undefined的类型保护和类型断言

开启 --strictNullChecks 选项后:

  1. 如果想要变量包含 nullundefined, 需要使用 联合类型 明确地包含它们:
    let s: string = '12'
    s = undefined  // error
    s = null  // error 
    let sn: string | null | undefined = '12'
    sn = undefined  // ok
    sn = null  // ok
    
  2. 可选参数和可选属性会自动添加上 | undefined
    function f(x: number, y?: number) {
      return x + (y || 0)
    }
    f(1, 2)
    f(1, undefined)
    
    
    interface Optional {
      name?: string
    }
    let o: Optional = {
      name: 'o'
    }
    o.name = undefined
    

null和undefined的类型保护

由于可以为 nullundefined 类型是通过 联合类型 来实现的,所以需要使用 类型保护 来去除 nullundefinednullundefined 的类型保护的写法与 JavaScript 中一致:

function checkType(arg: string | null | undefined) {
  if (arg === null) {
    console.log('null')
  } else if (arg === undefined) {
    console.log('undefined')
  } else {
    console.log('string')
  }
}

checkType('123')
checkType(null)
checkType(undefined)

也可以使用短路运算符去除 nullundefined

function f(s: string | undefined) {
  return s || 'default'
}

null和undefined的类型断言

如果编译器无法自动去除 nullundefined,可以通过 类型断言 手动去除,语法是添加 ! 后缀,arg! 表示从 arg 的类型中去除 nullundefined
下面的例子中使用了嵌套函数,编译器无法去除嵌套函数中的 null(除非是立即调用函数),此时就需要使用 null和undefined的类型断言语法 来手动去除 nullundefined

function getInitial(name?: string | null) {
  name = name || 'Bob'
  function upperCase() {
    // return name.toUpperCase()  // 错误,编译器无法去除 null 和 undefined
    return name!.toUpperCase()
  }
  return upperCase().charAt(0)
}

类型别名

接口 无法描述 交叉类型联合类型元组 等类型,这些时候通常需要使用到 类型别名
类型别名 用于给一个类型起一个别名,该类型通常为 交叉类型 ,联合类型, 对象字面量类型, 元组 等(也可以作用于原始类型,但是通常没什么用)。

注意: 类型别名 不会新建一个类型,而是创建一个名字引用那个类型。

interface Person{}
interface Serializable{}

type SerializablePerson = Person & Serializable  // 给交叉类型起别名

type PersonOrSerializable = Person | Serializable // 给联合类型起别名

type Alias = {  // 给对象字面量起别名
  name: string
}

type BookInfo = [string, string, number]  // 给元组起别名

type MyString = string  // 给原始类型string起别名,通常没什么意义

一个特殊的例子, 给 索引类型查询操作符(keyof) 得到的类型起别名:

interface Person {
  name: string
  age: number
}

type KeyOfPerson = keyof Person
let k: KeyOfPerson
k = 'name'  // ok
k = 'age'  // ok
k = 'abc'  // 错误,只能为Person属性名中的一个

和接口一样,类型别名 也可以是泛型:

// 泛型形式的 类型别名
type Container<T> = { value: T }

// 类型别名 在属性中引用自己
type Node<T> = {
  value: T
  left: Node<T>
  right: Node<T>
}

// 类型别名 与 交叉类型 一起使用
type LinkedList<T> = T & { next: LinkedList<T>}

接口和类型别名的区别

  1. 无法通过接口描述的类型,如 交叉类型联合类型元组 等, 通常会使用 类型别名
  2. 类型别名 不会创建新的类型,只是创建一个新的名字引用那个类型,而 接口 创建新的类型;
  3. 类型别名 无法被 extendsimplements (同时也不能 extends 其他类型),而 接口 可以。

字符串字面量类型

字符串字面量类型 用来指定字符串必须的固定值。实际应用中, 字符串字面量类型 可以与 联合类型 , 类型保护, 类型别名 配合使用,实现类似枚举类型的字符串:

type ResponseStatus = 'Ok' | 'NotFound' | 'Forbidden'

function getStatusCode(status: ResponseStatus): number {
   switch (status) {
    case 'Ok':
      return 200
    case 'NotFound':
      return 404
    case 'Forbidden':
      return 401
    default:
      throw new Error('unknown response status')
  }
}

console.log(getStatusCode('Forbidden'))  // 401
console.log(getStatusCode('BadRequest'))  // 错误,只能传递三种允许的字符串之一

可辨识联合

可以结合 单例类型, 联合类型, 类型保护, 类型别名 来创建一个叫做 可辨识联合 的高级模式。

可辨识联合 具有以下3个要素:

  1. 有单例类型的属性 - 可辨识的特征;
  2. 一个类型别名将这些类型联合起来;
  3. 此属性上的类型保护。

可辨识联合 例子如下:

interface Bird {
  kind: 'bird'
  tweet(): void
}

interface Cat {
  kind: 'cat'
  mew(): void
}

interface Duck {
  kind: 'duck'
  quack(): void
}

首先定义三个接口,每个接口都有 kind 属性但有不同的字符串字面量类型。 kind 属性称作 可辨识的特征,接下来使用 类型别名 将它们联合起来形成 可辨识联合

type Animal = Duck | Bird | Cat

然后就可以使用 可辨识联合

function getSound(animal: Animal): void {
  switch (animal.kind) {
    case 'bird':
      return animal.tweet()
    case 'cat':
      return animal.mew()
    case 'duck':
      return animal.quack()
    default:
      throw new Error('unknown animal')
  }
}

this类型

this类型 用于函数的返回值,能够保证接口的连贯性,实现链式调用。

class Calculator {
  protected value: number
  constructor(value: number) {
    this.value = value
  }

  printValue(): void {
    console.log(this.value)
  }

  add(num: number): this {
    this.value += num
    return this
  }

  multiply(num: number): this {
    this.value *= num
    return this
  }
}

new Calculator(1)
  .add(2)
  .multiply(3)
  .printValue()

继承自该类的子类可以继续使用这些方法,无需修改:

class ScientificCalculator extends Calculator {
  sin(): this {
    this.value = Math.sin(this.value)
    return this
  }
}

new ScientificCalculator(0)
  .sin()
  .add(2)
  .printValue()

索引类型

使用 索引签名,编译器能够检查使用了动态属性名的代码。一个常见的例子是从对象中选取属性的子集:

function pluck(obj, keys){
  return keys.map(key => obj[key])
}

使用 TypeScript 实现上面这个例子,需要使用 索引类型查询操作符( keyof T )索引访问操作符( T[K] )

function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map(key => obj[key])
}

索引访问操作符 的另一个例子,定义 getProperty() 方法获取对象上某个属性的值:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

索引类型和字符串索引签名

keyof TT[K] 可以与字符串索引签名进行交互:如果有一个带有字符串索引签名的类型,那么 keyof T 会是 stringT[K] 是索引签名的类型:

interface Map<T> {
  [prop: string]: T
}

let keys: keyof Map<number>  // keys 为 string 类型
let value: Map<number>['foo']  // value 为 number 类型

映射类型

一个常见的任务是将一个已知类型的所有属性变成可选的:

interface Person {
  name: string
  age: number
}

type PartialPerson = {
  name?: string
  age?: number
} 

TypeScript 提供了从旧类型中创建新类型的方式 —— 映射类型

type Partial<T> = {
  [P in keyof T]?: T[P]
}

映射类型 的语法与索引签名类似, 内部使用了 for...in

一些通用的 映射类型(如:Readonly<T>, Partial<T>, Pick<T, K extends keyof T>, Record<K extends string, T> )被包含进了TypeScript 标准库:

// Readonly<T>
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// Partial<T>
type Partial<T> = {
  [P in keyof T]?: T[P]
}

// Pick<T, K extends keyof T>
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// Record<K extends string, T>
type Record<K extends string, T> = {
  [P in K]: T
}

其中, Readonly, Partial, Pick同态的,因为映射只作用于 T 的属性,而 Record 不是同态的:

type ThreeThingRecord = Record<'prop1' | 'prop2' | 'prop3', string>

映射类型 的另一个例子,将 T[P] 包装进 Proxy<T> 中:

type Proxy<T> = {
  get(): T
  set(value: T): void
}

type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>
}

function proxify<T>(obj: T): Proxify<T> {
  let newObj = {} as Proxify<T>
  for (const key in obj) {
    newObj[key] = getProxyValue(obj, key)
  }

  function getProxyValue<T, K extends keyof T>(obj: T, key: K): Proxy<T[K]> {
    let addProxyValue: Proxy<T[K]> = {
      get(): T[K] {
        return obj[key]
      },
      set(value): void {
        obj[key] = value
      }
    }
    return addProxyValue
  }

  return newObj as Proxify<T>
}

let person = {
  name: 'Alice',
  age: 21
}

let proxifiedPerson = proxify(person)
console.log(person)  // { name: 'Alice', age: 21 }
console.log(proxifiedPerson)  // { name: { get: [Function: get], set: [Function: set] }, age: { get: [Function: get], set: [Function: set] } }
proxifiedPerson.age.set(22)
console.log(person)  // { name: 'Alice', age: 22 }
console.log(proxifiedPerson.age.get())  // 22

映射类型拆包推断

了解了如何包装一个类型的属性,那么接下来就是如何拆包。同态映射的拆包很简单:

function unproxify<T>(obj: Proxify<T>): T {
  let ret = {} as T
  for (const key in obj) {
    ret[key] = obj[key].get()
  }
  return ret
}

let originalPerson = unproxify(proxifiedPerson)
console.log(originalPerson)  // { name: 'Alice', age: 22 }

注意这个拆包推断只适用于同态的映射类型。非同态映射需要传入明确的类型参数。