15 官方工具类型

222 阅读6分钟

利用官方提供的工具类型,我们可以方便实现供更复杂的类型,避免重复造轮子。 工具类型划分为操作接口类型,联合类型,函数类型,字符串类型。

这些工具类型我们可以通过代码来模拟实现,常用到的关键字有: keyof, extends,in,-?readonly

概览

image.png

操作接口类型

Partial

将接口的所有属性变为可选的。

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

interface Person {
    name: string;
    age?: number;
}

type PartialPerson = Partial<Person>
// 相当于
interface IPartialPerson {
    name?: string;
    age?: number;
}

Required

与Partial相反,Required工具类型可以将给定类型的所有属性变为必填的(中括号后的-?意思是去掉可选标记)。

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

type RequiredPerson = Required<Person>;
// 相当于
class RequiredPerson {
    name: string;
    age: number;
}

Readonly

将给定类型的所有属性变为只读属性(在属性前面加上readonly关键字,表示该属性为只读属性)

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

type ReadonlyPerson = Readonly<Person>
// 相当于
class ReadonlyPerson {
    readonly name: string;
    readonly age?: number;
}

Pick

从给定类型中选出只读的键值,组成一个新的类型

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

type PickPerson = Pick<Person, 'name'>
// 相当于
class PickPerson {
    name: string;
}

Omit

从给定类型去除指定键值, 返回引的类型。

type Omit<T, K extends keyof any> = type Exclude<keyof T, K>
type newPeson = Omit<Person, 'name'>;
// 相当于
interface newPerson {
    age?: number;
}

keyof any

这里用到的keyof any指代可以作为对象键值的属性, 即string | number | symbol(目前,JavaScript仅支持string, number, symbol作为对象的键)。

type Keys = keyof any; // string | number | symbol;

操作联合类型

keyof Person 返回的是属性名联合类型: 'name' | 'age'

Exclude

从给定类型中排除某些属性,并返回新类型。

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

type T = Exclude<'a'|'b'|'c', 'a'>; // 'b' | 'c'
type newPerson = Omit<Person, 'weight'>
// 相当于
type newPerson = Pick<Person, Exclude<keyof Person, 'weight'>>
// 其中
type ExcludeKeys = Exclude<keyof Person, 'weight'>; // 'name' | 'age'

Extract

从给定类型中选出指定属性。 返回新的联合类型。

type Extract<T, U> = T extends U : T : never;
type T = Extract<'a' | 'b' | 'c', 'a'>; // 'a'

基于Extract和Exclude, 我们可以实现两个接口类型的交集及差集

Intersect 交集

返回新的接口(属性为两个接口的交集)

type Intersect<T, U> = {
    [K in Extract<keyof T, keyof U>]: T[K]
}

interface Animal {
    name: string;
    category: string;
}
// 联合类型'name' | 'age' 与 'name' | 'category' 取交集
type intersectPersonAnyAnimal = Intersect<Person, Animal>;
// 相对于
class intersectPersonAndAnimal {
    name: string
}

DifferenceSet 差集

返回新的接口(两个接口的差集)

type DifferenceSet<T, U> = {
    [K in Exclude<keyof T, keyof U>]: T[K]
} & {
    [K in Exclude<keyof U, keyof T>]: U[K]
}

// 相当于
type DifferenctSet<T, U> = Omit<T, keyof Intersect<T, U>> & Omit<U, keyof Intersect<T, U>>

NonNullable

从联合类型中去除null或者undefined类型。

type NonNullable = T extends null | undefined ? never : T;
// 等价于
type NonNullable<T> = Exclude<T, null | undefined>;

type T = NonNullable<string | number | undefined | null>; // string | number

Record

生成接口类型, 使用传入的泛型参数分别最为接口类型的属性和值。

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

type MenuKey = 'home' | 'about' | 'more';
interface Menu {
    label: string;
    hidden?: boolean
};

const menuItems: Record<MenuKey, Menu> = {
    home: {label: 'home'},
    about: {label: 'about'},
    more: {label: 'more', hidden: true},
}

操作函数类型

infer 参数引用标记,后面会再用到该标记参数。

ConstructorParameters

ConstructorParameters用来获取构造函数的构造参数, 而ConstructorParameters类型的实现则需要使用infer关键字推断构造参数的类型。

关于infer关键字, 可以把它当做简单的模式匹配来看待,如果真实的参数类型和infer匹配的一致, name就返回匹配到的这个类型。

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

class Person {
    constructor(name: string, age? number){}
}

type T = ConstructorParameters<typeof Person>; // [name: string, age?: number] 

ConstructorParameters泛型接收了一个参数, 并且限制这个参数需要实现构造函数。 于是, 通过infer关键字匹配了构造函数内的构造参数,并且返回了这些参数。 因此, 可以看到最后一行匹配了Person构造函数的两个参数,并返回了一个元组类型[string, number]给类型别名T.

Parameters

作用域ConstructorParameters类似, Parameters可以用来获取函数的参数并返回序列

type Parameters<T extends (...args: any) => any> = 
    T extends (...args: infer P) => any 
    ? P
    : never;
    
 type T0 = Parameters<() => void>; []
 type T1 = Parameters<(x: number, y?: string) => void>; // [x: number, y?: string]

ReturnType

用来获取函数的返回类型.

type ReturnType<T extends (...args: any) => any> =
    T extends (...args: any) => infer R
    ? R
    : any;
    
type T0 = ReturnType<()=> void>; // void
type T1 = ReturnType<() => number>; //number

ThisParameterType

获取函数的this参数类型

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any
    ? U
    : unknowntype T = thisParameterType<(this: Number, x:number) => void>; // Number

ThisType

在对象字面量中指定this的类型。 ThisType不返回转换后的类型, 而是通过ThisType的泛型参数指定this的类型。

需要开启noImplicitThis的ts配置。 它只是提供了一个空的泛型接口, 仅可以在对象字面量上下文中被TypeScript识别。interface ThisType<T>{}, 也就是说该类型的最有相当于任意空接口,

type ObjectDescriptor<D, M> = {
    data?: D,
    methods: M & ThisType<D, M>; // methods中this的类型是 D & M
}

function makeObject<D, M>(desc: ObjectDescriptor<D,M>): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return {...data, ...methods} as D & M;
}

const obj = makeObject({
    data: {x: 0, y: 0},
    methods: {
        moveBy(dx: number, dy:number) {
            this.x += dx; // this => D & M
            this.y += dy; // this => D & M
        }
    }
})

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5)

OmitThisParameter

去除函数类型中的this类型, 如果传入的函数类型没有显示声明this类型,那么返回的仍是原来的函数类型。

type OmitThisParameter<T> = unknown extends ThisParameterType<T>
    ? T
    : T extends (...args: infer A) => infer R
        ? (...args: A) => R
        : T;
  
type T = OmitThisParameter<(this: Number, x: number) => string>

ThisParameterType 类型的实现如果传入的泛型参数无法推断 this 的类型,则会返回 unknown 类型。在OmitThisParameter 的实现中,第一个条件语句如果传入的函数参数没有 this 类型,则返回原类型;否则通过 infer 分别获取函数参数和返回值的类型构造一个新的没有 this 的函数类型,并返回这个函数类型。

操作字符串类型

模板字符串

TypeScript 4.1之后, 提供了Uppercase, Lowercase, Capitalize, Uncapitialize这四种字符串的类型。

// 转换字符串字面量到大写字母
type Uppercase<S extends string> = intrinsic;
// 转换字符串字面量到小写字母
type Lowercase<S extends string> = intrinsic;
// 转换字符串字面量的第一个字母为大写字母
type Capitalize<S extends string> = intrinsic;
// 转换字符串字面量的第一个字母为小写字母
type Uncapitalize<S extends string> = intrinsic;
type T0 = Uppercase<'Hello'>; // => 'HELLO'
type T1 = Lowercase<T0>; // => 'hello'
type T2 = Capitalize<T1>; // => 'Hello'
type T3 = Uncapitalize<T2>; // => 'hello'

这 4 种操作字符串字面量工具类型的实现都是使用 JavaScript 运行时的字符串操作函数计算出来的,且不支持语言区域设置。以下代码是这 4 种字符串工具类型的实际实现。

function applyStringMapping(symbol: Symbol, str: string) {
 switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
   case IntrinsicTypeKind.Uppercase:
     return str.toUpperCase();
   case IntrinsicTypeKind.Lowercase:
     return str.toLowerCase();
   case IntrinsicTypeKind.Capitalize:
     return str.charAt(0).toUpperCase() + str.slice(1);
   case IntrinsicTypeKind.Uncapitalize:
     return str.charAt(0).toLowerCase() + str.slice(1);
 }
 return str;
}

在上述代码中可以看到,字符串的转换使用了 JavaScript 中字符串的 toUpperCase 和 toLowerCase 方法,而不是 toLocaleUpperCase 和 toLocaleLowerCase。其中 toUpperCase 和 toLowerCase 采用的是 Unicode 编码默认的大小写转换规则。