Typescript学习(六) 类型工具

92 阅读11分钟

我们前面介绍了各种类型, 包括原始类型、字面量类型、也了解了类型层级的基本概念, 这些都是组成类型编程世界的基础, 属于一个基本单元; 接下来要说的类型别名、映射类型都可以看作是组织起这些基本单元的工具, 因此亦可称之为工具类型;

创建型类型工具

类型别名

先来看看类型别名, 所谓的类型别名, 其实就是利用一个关键字type, 来对我们前面说的一些基础类型进行封装, 从而形成新的类型;

例如, 我们可以使用类型别名type关键字, 来声明一些新的类型

type person = 'male' | 'female' | 'renyao'
type basicType = string | number | boolean | undefined | null

这样, 当我们使用这些联合类型的时候, 就可以直接使用类型别名了

let man:person = 'male'
let str:basicType = 'this is string'

除此之外, 还能和泛型配合使用, 所谓泛型, 其实就是'类型的参数', 此时的类型别名, 就是一个工具类型, 工具类型就好比是一个函数, 而泛型, 就是这个'函数'的'入参':

// 创建新的数组类型
type newArr<T> = T[]

交叉类型

我们前面学习了联合类型, 联合类型其实是一种'或'的逻辑, 也就是只要实现联合类型中的一个成员, 就算是符合这个联合类型; 而接下来要介绍的交叉类型,其实就是'且'的逻辑, 也就是必须符合交叉类型中的每一个成员, 才算是符合这个交叉类型;

来看看联合类型的交叉类型

type smallPower = 'military' | 'money' | 'culture' | 'small'

type superPower = 'military' | 'money' | 'culture' | 'territory'

type power = power & superPower // "military" | "money" | "culture"

以上案例中, smallPower 和 superPower的交叉类型, 其实就是两者的交集("military" | "money" | "culture"), 即两者都有的部分;

如果我们把两个毫不相关的类型求一下交叉类型, 结果会如何呢?

type strangeType = string & number // never

可以看到, 此时的类型只能是never了, 因为string 和 number是两种毫无关联的类型

综合一句: 交叉类型, 就是求成员之间的共同点, 如果实在没有共同点, 那就是never, 即 不存在的类型;

索引类型

所谓的索引类型, 其实包含三个部分: 索引签名类型、索引类型访问、索引类型查询, 他们的共同点就是, 允许在编译阶段, 动态访问对象类型的键和值, 从而提高代码的可维护行;

索引签名类型

索引签名类型其实是定义一个对象键/值类型的语法

interface ObjectType{
  [key: string] : string;
}

let obj:ObjectType = {
  propA: 'this is string'
}

type PhoneBook = {
  [name: string]: number
}

let myPhoneBook = {
  jack: 1111111,
  rose: 2222222
}

这样, 也就相当于定义了ObjectType这个对象类型中的所有键都是string类型, string类型的键所对应的值也是string类型; 说白了, 索引签名类型提供了一个模糊的类型范围, 使我们不需要去指定每一个成员的键值类型, 简化了类型声明过程; 当然, 也可以去指定具体的类型

interface ObjectType{
  [key: string] : string;
  100: 'The key is number, but the value is string'
}

type type1 = ObjectType[100] // The key is number, but the value is string
type type2 = ObjectType['100'] // The key is number, but the value is string

注意, 虽然我们的索引签名类型规定了键必须是string, 但是, 由于在javascript中, obj[key]这种形式访问对象属性时, 即使key是数字, 也会被转为字符串, 正是基于这个特性, 我们这个100的键, 也能被Typescript接受, 也就是说, Typescript认为这是一个‘string’, 因此, 它的值也必须符合实现的约定, 是一个string; 如果我们将其值也改为数字, 则会报错

interface ObjectType{
  [key: string] : string;
  100: 200 // 类型200的属性100不能赋给'string'索引类型的'string'
}

索引签名类型还能使用联合类型

interface ObjectType{
  [key: string|symbol]: string;
}

这里要注意了, 索引签名参数只能是string、number、symbol、文本模版类型, 除此之外, 都不行!

interface ObjectType{
  [key: string|symbol|object]: string; // 报错
}

索引类型查询

所谓的索引类型查询, 其实就是利用keyof操作符来获取对象类型中的键

interface Student{
  name: string;
  age: number;
  gender: 'male' | 'female'
}

type keys = keyof Student // 'name' | 'age' | 'gender'

let requiredKeys:keys = 'age'

索引类型访问

我们在javascript中, 会用obj[key]的方式访问一个对象上的属性, 同样的, 在Typescript中, 也一样可以用这种方式访问类型

interface Student{
  name: string;
  age: number;
  gender: 'male' | 'female'
}

type nameType = Student['name'] // string

这里注意了, 这里的name, 是类型, 返回值, 也是类型, 而不是一个实实在在的值!

如果索引访问中的键是一个联合类型, 那么, 其返回的类型, 也将是一个联合类型

interface Person{
  name: string;
  age: number;
  isAduit: boolean
}

type nameType = Person[keyof Person] // string | number | boolean

这里keyof Person其实就是name|age|isAduit, 而它们对应的类型也将在索引类型访问后组成一个联合类型

映射类型

映射类型, 是Typescript中的一种特殊语法, 它可以将一个类型的每个属性进行遍历, 改变这个属性的类型乃至修饰符

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

type data = {
  NO: number,
  name: string
}

type NewType = MyPartial<data>
/**
  * type NewType = {
  *  NO?: number | undefined;
  *  name?: string | undefined;
	*	}
*/

上面案例中, 通过in关键字, 将data类型中的每一个属性, 都变为可选属性, 同样, 也可以将所有属性变为同一种类型

type AllBeNumber<T> = {
  [K in keyof T]?: number;
};
/**
 * type NumberType = {
 *    NO: number;
 *    name: number;
 *  }
 */

类型保护型类型工具

上面介绍的, 类型别名、交叉类型、索引类型、映射类型, 它们都是创建一种类型的工具, 也可以称之为创建型类型工具. 接下来要介绍的类型工具, 主要是起类型保护, 即防止一些因类型偏差而导致的错误

类型守卫

所谓类型守卫, 其实就是Typescript能够在编译阶段, 根据代码的一些类型相关的逻辑, 实时更新或者说收窄变量的类型并在程序中提供更准确的类型推导的技术, 也可以称之为类型推导, 例如以下操作:

function fn (params: string | number | boolean) {
  if (typeof params == 'string') {
  } else if (typeof params === 'number') {
  } else if (typeof params === 'boolean'){
  } else {
    let a:never = params // 不报错
  }
}

以上代码中, 最后的判断分支中的params只剩下了never, 因为前面的实际判断逻辑已经把已有的情况都进行了判断和使用, 因此, 就只有never, 这个在之前never的例子中有说明; 这里我们用到了typeof 操作符, 类型守卫除了typeof的方式, 还有自定义类型保护、instanceof、in、类型断言守卫等等

typeof

说到typeof, 这个在JavaScript中已经存在了, 在Typescript中, 其作用也是一样的, 但是, 它更加强大了, 在JavaScript中, typeof其实只能在逻辑代码中找出仅有的那些类型, 比如: string/number/boolean/object之类的等等; 在Typescript中, 它不仅能在逻辑代码中运行, 还能在类型代码中运行, 而且, 类型更加丰富, 更加精确;

const desc1 = 'jack' // desc1类型为'jack'
let str1:typeof desc1 = 'jack1' // 报错, 因为str1只能接受字面量

let desc2 = 'jack' // desc2类型为string

let str2:typeof desc2 = 'jack1' // 正确

上面代码中, 当我们用const声明一个变量的值为一个字符串时, 在类型代码中, 使用typeof+变量, 就会得到一个精确的字面量类型'jack', 因为const声明的变量不会再发生改变了, 因此, Typescript可以将其收窄为具体的字面量类型; 而如果用let来声明, 意味着这个变量还能变, 因此, Typescript类型代码中执行typeof + 变量后, 只会给出一个粗略的string类型, 而不是字面量类型;

当然, 我们都知道, const只能管住基础类型, 如果是一个对象, 它的内部属性一样是可以更改的, 所以, 如果我们使用const 声明一个对象, 其属性, 在类型代码中执行typeof, 也不会得到精确的字面量类型

const obj = {
  name: 'jack',
  age: 18,
  isChild: false
}

let person:typeof obj
/**
 * let person: {
    name: string;
    age: number;
    isChild: boolean;
  }
 */

小节: Typescript中的操作符typeof和Javascript中的具有相同的功能, 但是, Typescript中的typeof显然能在条件允许(const声明变量不再发生改变)的情况下尽可能精确地返回类型;

自定义类型保护

前面fn函数的例子中, 如果我们稍微改造一下:

function isNumber (params) {
  return typeof params === 'number'
}
function fn (params: string | number | boolean) {
  if (typeof params == 'string') {
  } else if (isNumber(params)) {
  } else if (typeof params === 'boolean'){
  } else {
    let a:never = params // 不能将类型number分配给类型never
  }
}

我们只是封装了下number的类型判断, 但是报错了, 类型推导失效了! 因为尽管我们在isNumber函数中判断了params的类型为number, 并返回了true, 但是在编译器视角, 这里只是一个函数调用, 并不能推导出, 类型number已经被使用; 因此, 我们需要使用类型谓词 参数 + is + 类型, 来进行自定义类型保护, 即明确告知这里的params类型为number

function isNumber (params): params is number {
  return typeof params === 'number'
}
function fn (params: string | number | boolean) {
  if (typeof params == 'string') {
  } else if (isNumber(params)) {
  } else if (typeof params === 'boolean'){
  } else {
    let a:never = params // 不报错
  }
}

当然, 类型谓词也可以是其他类型

function isNumber (params): params is string | number | boolean {
  return typeof params === 'number'
}
function fn (params: string | number | boolean) {
  if (isNumber(params)) {
  } else {
    let a:never = params // 不报错
  }
}

in

简单的类型, 我们可以通过typeof来进行区分, 但是如果我们要区分复杂的类型, 比如一个对象, 那typeof显然就没什么用了, 这时, 我们需要用到in 关键字来做一个类型的区分

interface Dog {
  bite: () => void
}

interface Cat {
  scratch: () => void
}

function buyPets (pet: Dog | Cat) {
  if ('bite' in pet) {} 
  else if ('scratch' in pet) {}
  else {
    let a:never = pet
  }
}

注意, 我们使用的in的操作对象必须是可辨识的属性, 也就是某个对象类型中独一无二的属性, 否则, 则有可能出现问题

interface Dog {
  bite: () => void;
  color: string;
}

interface Cat {
  scratch: () => void;
  color: string;
}

function buyPets (pet: Dog | Cat) {
  if ('color' in pet) {
    pet.bite() // 此时的pet仍为Dog | Cat, 所以, 有可能不存在bite
  } else {
    let a:never = pet
  }
}

instanceof

除了以上几种, 还有一种用于区分不同类型实例的, 也就是instanceof, 它可以识别某个对象是否继承自某个类;

class Animal {
  color:string;
  constructor (color) {
    this.color = color
  }
}

class Dog extends Animal {}

class Building {
  height:number;
  constructor (height) {
    this.height = height
  }
}

class Cabin extends Building {}


function fn (params: Animal | Building) {
  if (params instanceof Animal) {
    console.log('颜色为:' + params.color)
  } else if (params instanceof Building) {
    console.log('高度为' + params.height + '米')
  } else {
    let neverData:never = params
  }
}

fn(new Dog('yellow'))

fn(new Cabin(100))

以上案例中, 我们定义了fn的参数为Animal | Building类型, 内部的逻辑是判断入參是集成自哪个类,这同样也是类型守卫的形式之一;

断言守卫

当类型守卫遇到不符合条件的判断时, 类型推导会在后续判断中, 会将该类型剔除掉, 从而保证接下来代码的准确性; 而接下来要讨论的断言守卫, 则是用另一种方式在为我们的后续代码保驾护航, 断言守卫会使用asserts关键字, 来提前告诉编译器, 即将传入的参数是什么类型; 先来看一个案例

function fn (value) {
  if (typeof value !== 'number') {
    throw new Error('it should be number!')
  }
  return value.toFixed()// value类型为number
}

以上代码中, 我们可以知道, 如果value不是number类型, 那么肯定报错, 当然, ts也能够推断, fn函数最后的value肯定是number类型, 这个没毛病, 但是, 如果我们把类型判断封装进一个函数中呢?

const params:any = 'jack'

function fn (value) {
  assert(typeof value === 'number')
  return value.toFixed() // value类型为any
}

function assert (condition) {
  if (!condition) {
    throw new Error('it should be number!')
  }
}

我们会发现, 没有报错, 但是, value变为any类型了!! 那这又有什么意义呢? 我们的类型推导失效了! 此时就需要用到asserts, 即 断言守卫了!

const params:any = 'jack'

function fn (value) {
  assert(typeof value === 'number')
  return value.toFixed() // value为number类型
}

function assert (condition):asserts condition {
  if (!condition) {
    throw new Error('it should be number!')
  }
}

我们在assert方法的后面加上了asserts condition, 即判断typeof params === 'number'是否为true, 如果为true , 则后续代码中的params肯定就是number类型; 此时, 最后的value的类型就变为了正确的number类型了!

我们还能和类型谓词结合起来, 使代码更加清晰明了, 即直接断言一个类型为number类型

const params:any = 'jack'

function fn (value) {
  assert(value)
  return value.toFixed() // value为number类型
}

function assert (value):asserts value is number {
  if (typeof value !== 'number') {
    throw new Error('it should be number!')
  }
}