给团队做次TypeScript分享(七)—— 高级类型

293 阅读10分钟

高级类型

联合类型

联合类型表示一个值可以是几种类型之一。

type T = string | number
let val1:T = '' // ok
let val2:T = 1  // ok
type T1 = 'a' | 'b'
type T2 = 'a' | 'c'
type T3 = T1 | T2  // 'a' | 'b' | 'c'

如果一个值是联合类型,那么只能访问此联合类型的所有类型里共有的成员。

interface Bird {
  fly: () => void;
  layEggs: () => void;
}

interface Fish {
  swim: () => void;
  layEggs: () => void;
}

function getAnimal() : Bird | Fish{
  ...
}

getAnimal().layEggs(); // ok
getAnimal().fly(); // error    

交叉类型

交叉类型是将多个类型合并为一个类型。

把多种类型叠加到一起成为一种类型,包含所属所有类型的特性。

interface IStyleShape {
  width: string;
  height: string;
}

interface IStyleColor {
  background: string;
  color: string;
}

let myDomStyle: IStyleShape & IStyleColor = {
  width: "100px",
  height: "100px",
  background: "red",
  color: "#666",
};
// myDomStyle 必须含有width、height、background、color四个属性

如果两个类型有相同的 key 并且不同的情况下,则交叉后该key的类型为never

interface IStyleShape {
  width: string;
  value: string
}

interface IStyleColor {
  background: string;
  value: number
}

type T = IStyleShape & IStyleColor

let val:T = {
  width: '100px',
  background: '#666',
  value: (function () {
    throw new Error()
  })() // value 类型为never
}

联合类型进行交叉

type T1 = 'a' | 'b'
type T2 = 'a' | 'c'
type T3 = string | number

type crossType1 = T1 & T2 // 'a'
type crossType2 = T1 & T3 // 'a' | 'b'

类型关键字

ts 提供了多种用于类型的关键字,除了上面泛型中提到的 extends 外,主要还有

  • 类型谓语 is
  • typeof
  • instanceof
  • keyof
  • T[K]
  • in
  • infer

这些关键字中类型谓语 is、typeof、instanceof 主要用于类型保护(或者叫类型收窄);

在上面联合类型的例子中,我们想通过 getAnimal() 调用 fly 或是 swim 时应该怎么做呢?可以通过类型断言的方式

let animal = getAnimal();
if((<Bird>animal).fly){
  (<Bird>animal).fly();
}else{
  (<Fish>animal).swim()
}

这种方式需要多次使用类型断言,比较麻烦,如果在if else 分支中可以明确确认 animal 的具体类型就方便很多了,这时就需要类型保护的表达式。

所谓 ts 的类型保护,其实就是通过一些表达式来确保在某个作用域中有确定的类型。

let myDiv = document.getElementById('app') // 类型为 HTMLElement | null
if(myDiv){
  console.log(myDiv.scrollHeight)
}
// 例如上面的例子中 myDiv null 或者 dom元素类型,这时候在 if 条件表达式中做了一层类型收窄,以致在if的分支中将类型 HTMLElement | null 收窄为 HTMLElement

类型谓语 is

类型谓语是 parameterName is Type 这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

例如上面联合类型的例子,我们可以写一个 isBird 函数

function isBird(animal: Bird | Fish) : animal is Bird{
  return (<Bird>animal).fly !== undefined
}

这样就可以调用 fly 方法了

let animal = getAnimal()

if(isBird(animal)){
  animal.fly() // ok
}else{
  animal.swim() // ok
}

上面例子中通过类型谓语 is, if 分支中 ts 知道 animal 一定是 Bird 类型,else 分支中一定是 Fish 类型

上面例子类型谓语 is 就通过区分类型起到类型保护的作用

可能会觉得这个返回不是布尔值吗,如果将 isBird 函数返回值改为 布尔值,则起不到类型保护的作用

function isBird(animal: Bird | Fish) : boolean {
  return (<Bird>animal).fly !== undefined
}
if(isBird(animal)){
  animal.fly() // error 类型“Bird | Fish”上不存在属性“fly”。
}else{
  animal.swim() // error 类型“Bird | Fish”上不存在属性“swim”
}

typeof

语法为 typeof v === "typename" 和 typeof v !== "typename"

typeof 也可用于类型保护

其中 typename 只能是 "number", "string", "boolean" 或 "symbol",虽然 typeof v 可以和其他字符串进行比较,但是 ts 不会把那些表达式识别为类型保护。

function calculate(val: string | number){
  if(typeof val === 'string'){
    return val.length
  }else{
    return val.toFixed()
  }
}

instanceof

instanceof类型保护是通过构造函数来细化类型的一种方式。

instanceof 右侧只能是构造函数

class Bird {
  fly() {
    console.log("fly");
  }
}

class Fish {
  swim() {
    console.log("swim");
  }
}

function fun(animal: Bird | Fish){
  if(animal instanceof Bird){
    animal.fly()
  }else{
    animal.swim()
  }
}

keyof

索引类型查询操作符

对于任何类型 Tkeyof T的结果为 T上已知的公共属性名(注意是公共属性)的联合。

interface User{
  name : string,
  age : number
}  
let userProps: keyof User // 相当于 let userProps: 'name' | 'age'

T[K]

索引访问操作符

类似获取对象索引值的方式,ts 类型中,T[K] 可以从 T 类型中获取 K 属性的类型

interface User{
  name : string,
  age : number
}
 
type nameString = User['name'] // 相当于 type nameString = string
let okVariable: nameString = '' // ok
let errorVariable: nameString = 121 // error

如果T[K]中的 K 不存在 T 的key中,则会报错

type t = User['height'] // error 类型“User”上不存在属性“height”

结合 T[K] 和上面的 keyof,可以发现,

T[keyof T]相当于 T 上的所有key类型组成的联合类型,例如

interface User {
  name: string,
  age: number,
  unmarried: boolean,
  height: number
}

type t = User[keyof User] // type t = string | number | boolean

in

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

映射类型中可以使用 in 将旧类型的每个属性以同种方式都转化为新类型。

type Props = 'name' | 'age';
type UserType = { [K in Props]: string };
// UserType 相当于
type UserType = { 
	name: string,
    age: string,
};

语法为 K in Keys

Keys 为联合类型,是要迭代的属性名集合,K 是类型变量,代表每个属性。

例如我们可以将接口的每个类型变为可选

interface User{
  name: string
  age: number
}

type myPartial<T> = {
  [K in keyof T]? : T[K]
}
// myPartial 相当于
type myPartial<T> = {
  name?: string
  age?: number
}

let myUser:myPartial<User> = {}

extends

前面泛型中说过,可用于类型约束

除了约束,T extends K 也可以判断 T 能否分配给 K ,做类型判断,格式类似三元运算符

type Allot<T> = T extends 'a' | 'b' ? T : null

type aType = Allot<'a'>  //  'a'  'a' 类型可以分配给 'a' | 'b' 类型
type nullType = Allot<'c'> // null 'c' 类型不能分配给 'a' | 'b' 类型所以返回 null

如果 T extends K ,T 是联合类型,这时会依次判断该联合类型的所有子类型是否可以分配给 K (分发),依次判断完会将所有结果组合为新的联合类型

// 例如用上面例子的 Allot
type uniteType = Allot<'a' | 'b' | 'c'> // 相当于 'a' | 'b' | null
type toArray<T> = T extends any? T[] : never
type T1 = toArray<string | number> //  type T1 = string[] | number[]

可以使用元组避开这种行为

type toArray<T> = [T] extends any? T[] : never
type T1 = toArray<string | number> // type T1 = (string | number)[]

infer

infer 声明会引入一个待推断的类型变量,

infer关键词只能在 extends 条件类型上使用,不能在其他地方使用。

type ArrayItemType<T> = T extends (infer P)[] ? P : null
type T0 =  ArrayItemType<string[]> // string
type T1 =  ArrayItemType<any[]> // any
type T2 =  ArrayItemType<100> // null
type T3 =  ArrayItemType<[string,number]> // string | number

例如上面例子中的 ArrayItemType<T> 中 T extends (infer P)[] 这里其实判断了传入的 类型变量 T 是不是数组,如果是,类型推断 P 会根据 T 推断出数组的组成类型并返回

一个多层嵌套条件类型的推断例子

type Unpacked<T> = T extends (infer U)[]
  ? U
  : T extends (...args: any[]) => infer U
  ? U
  : T extends Promise<infer U>
  ? U
  : T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string

相同的类型变量都处于协变位置,推断出的类型是联合类型

type Unite<T> = T extends { a: infer U; b: infer U } ? U : never;
type T1 = Unite<{ a: string; }>; // never
type T2 = Unite<{ a: string; b: string }>; // string
type T3 = Unite<{ a: string; b: number }>; // string | number
type T4 = Unite<{ a: string; b: number; c: boolean }>; // string | number

相同的类型变量都处于逆变位置,推断出的类型是交叉型

type Cross<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;
type T1 = Cross<{ a: (x: string) => void; b: (x: string) => void }>; // string
type T2 = Cross<{ a: (x: string) => void; b: (x: number) => void }>; // never(string & number 就是 never)
type T3 = Cross<{ a: (x: {a: string }) => void; b: (x: { b: number}) => void }>; // { a: string } & { b: number }

ts 预定义的工具类型

ts 内部有预定义了一些工具类型,常用的有:

Partial<T>

将T类型的所有属性都改为可选

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

例如

interface User{
  name: string
  age: number
}

let people: Partial<User> = {} // ok
people.age = 19 // ok

Required<T>

将T类型的所有属性都改为必选

type Required<T> = {
    [P in keyof T]-?: T[P];
};

例如

interface User{
  name?: string
  age?: number
}

let peopleOne: Required<User> = {
  name: 'jack',
  age: 18,
} // ok

let peopleTwo: Required<User> = {
  name: 'mike',
} // error 缺少 age 属性

Readonly<T>

将T类型的所有属性都改为只读

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

例如

interface User{
  name: string
  age: number
}

let people: Readonly<User> = {
  name: 'jack',
  age: 25
}

people.age = 19 // error, 不可修改

ReadonlyArray<T>

类型为 T[] ,且数组在初始化后不可修改

源码这个有点长,简单看下就行

interface ReadonlyArray<T> {
    readonly length: number;
    toString(): string;
    toLocaleString(): string;
    concat(...items: ConcatArray<T>[]): T[];
    concat(...items: (T | ConcatArray<T>)[]): T[];
    join(separator?: string): string;
    slice(start?: number, end?: number): T[];
    indexOf(searchElement: T, fromIndex?: number): number;
    lastIndexOf(searchElement: T, fromIndex?: number): number;
    every<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): this is readonly S[];
    every(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean;
    some(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean;
    forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void;
    map<U>(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any): U[];
    filter<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): S[];
    filter(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): T[];
    reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T;
    reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;
    reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;
    reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T;
    reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;
    reduceRight<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;
    readonly [n: number]: T;
}

例如

let arr : ReadonlyArray<string> = ['js', 'ts']
arr[0] = 'vue' // error 不能修改
arr.length = 5 // error 不能修改长度
arr.push("react") // 没有 push 方法

Pick<T,K>

从类型 T 提取部分 K 属性作为新的类型

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

例如

interface User{
  name: string
  age: number
  gender: string
}

let peopleOne: Pick<User, 'name'> = {
  name: 'jack',
}
people.age = 19 // error, 类型“Pick<User, "name">”上不存在属性“age”

let peopleTwo: Pick<User, 'name' | 'gender'> = {
  name: 'jack',
  gender: 'man'
}

Exclude<T, U>

从T中排除那些可赋给U的类型

type Exclude<T, U> = T extends U ? never : T;

例如

type strExclude = Exclude<'a'|'b'|'c', 'b'|'c'|'d'> // 'a'

其实上面的例子应该是返回 "a" | never,但是never 与其他类型联合会去掉 never

type T1 = "a" | never // 相当于 type T1 = "a"

Extract<T, U>

从T中提取那些可以赋值给U的类型

type Extract<T, U> = T extends U ? T : never;

例如

type strExtract = Extract<'a'|'b'|'c', 'b'|'c'|'d'> // 'b' | 'c'

Omit<T, K>

从 T 类型中去除 K 属性

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

上面的 keyof any 相当于 string | number | symbol,因为作为索引的类型只能是 string | number | symbol

例如

interface User{
  name: string
  age: number
  gender: string
}

type omitExample = Omit<User, 'name' | 'age'>

let people: omitExample = {
  gender: 'man',
}

Record<K, T>

构造具有类型 T 的一组属性 K 的类型

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

例如

type recordExample = Record<'name' | 'age' | 'gender', string>

let people:recordExample = {
  name: 'jack',
  age: '20',
  gender: 'man',
}

借用这个工具,我们也可以方便的定义一个不确定属性类型的对象类型

type objType = Record<string, any> 
// 相当于
type objType = {
    [x: string]: any;
}

ReturnType<T>

获取函数类型的返回类型

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

例如

type funType = (name:string, age:number) => string
type InstanceTypeExample2 = ReturnType<funType> // string

Parameters<T>

返回函数类型的参数组成的元组

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

例如

type funType = (name:string, age:number) => boolean
type parametersExample = Parameters<funType> // [string, number]

NonNullable<T>

从 T 中排除null和undefined

type NonNullable<T> = T extends null | undefined ? never : T;

例如

type myType = string | number | undefined | null
type noNullMyType = NonNullable<myType> // string | number

InstanceType<T>

获取构造函数类型的返回类型

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

通过观察代码可以发现 T extends abstract new (...args: any) => any 这里T被约束为构造函数,为什么要用 abstract 抽象类关键字呢

因为当类型定义为抽象类,那么既可以赋值为抽象类,也可以赋值为普通类

 class Animal {}
 abstract class AnimalAbstract {}
 
 const myAnimal1: typeof Animal = Animal // ok
 const myAnimal2: typeof Animal = AnimalAbstract // error 无法将抽象构造函数类型分配给非抽象构造函数类型

 const myAnimal3: typeof AnimalAbstract = Animal // ok
 const myAnimal4: typeof AnimalAbstract = AnimalAbstract // ok

InstanceType例子

interface User {
  new (name:string, age:number) : {name:string, age:number}
}

type InstanceTypeExample = InstanceType<User> // { name:string, age:number }


class User {
  name: string | undefined
  age: number | undefined
  constructor (name: string, age: number){
    this.name = name
    this.age = age
  }
}
type InstanceTypeExample = InstanceType<typeof User> // User
// 例如在基于vue3的element-plus框架中,定义模板引用的 ref
<template>
	<el-form
    	ref="formRef"
	>
    	...    
    </el-form>
</template>
<script lang="ts" setup>
	import type { ElForm } from 'element-plus'
	const formRef = ref<InstanceType<typeof ElForm>>()
</script>        

ConstructorParameters<T>

返回构造函数类型的参数类型组成的元组

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

例如

interface User {
  new (name:string, age:number) : {name:string, age:number}
}
type ConstructorParamExample = ConstructorParameters<User> // [string, number]