TypeScript类型体操(一)

所谓类型体操,就是仅基于TypeScript的各种类型操作来实现一些工具类型,使用TypeScript的类型推导能力来运行具体的逻辑。借助泛型,模板字符串,推断等特性,可以绕开一些限制,构建出非常复杂的类型。知名开发者antfu在GitHub上发起了一个类型体操挑战项目type-challenges,里面收录了一系列由易到难的类型体操挑战,通过完成这些挑战可以极大地提升对TypeScript的理解,让我们在实际开发中能构建出更健壮的类型系统。

压缩体操基础

条件类型

TypeScript中的条件类型看起来有点像三元表达式:condition ? trueExpression : falseExpression​:

1
SomeType extends OtherType ? TrueType : FalseType;

extends​ 左边的类型可以赋值给右边的类型时,就会得到第一个分支(“true” 分支)的类型;否则,会获得“false”分支。

比如:

1
type Flatten<T> = T extends any[] ? T[number] : T;

Flatten​类型可以将一个数组类型展平为它们的元素类型。

条件类型提供了一种infer​关键字,可以让我们在条件语句中进行类型推断的方法,例如,我们可以推断出Flatten​中的元素类型,而不是使用索引访问类型来获取:

1
type Flatten<T> = T extends Array<infer Item> ? Item : Type;

这里使用了infer​引入了一个名为Item​的类型变量,而不是指定如何在true分支中通过索引来访问类型。

keyof

keyof​运算符生成一个对象类型的键的字符串或数字字面联合类型:

1
2
3
4
type Point = { x: number; y: number };
// Point = "x" | "y"
type Arrayish = keyof { [n: number]: unknown };
// Arrayish = number

typeof

JavaScript已经有一个typeof运算符了,TypeScript也有一个编译时的typeof,可以在类型的上下文中获取一个变量或属性的类型:

1
2
let s = "hello";
let n: typeof s; // string

这在这种普通用法上不是很明显,用在复杂的类型时是十分有用的,比如我们第一次尝试使用ReturnType时,可能是这种用的:

1
2
3
4
5
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>;
// “f”表示值,但在此处用作类型。是否指“类型 f”?ts(2749)

这里会报错,是因为值和类型不是同一回事,我们要引用f的类型,因此用typeof:

1
2
3
4
5
6
7
8
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
// type P = {
// x: number;
// y: number;
// }

模板字符串类型

TypeScript中的模板字符串类型和JavaScript中的模板字符串有相似的语法:

1
2
type Person = "Wenwazi";
type Greeting = `I love ${Person}.`; // I love Wenwazi.

当在插值位置使用联合类型时,得到的类型是由每个联合成员表示的每个可能得字符串的集合:

1
2
3
4
5
6
7
8
9
type DB = {
mysql: "MySQL",
postgres: "PostgreSQL",
sqlite: "SQLite",
mssql: "Microsoft SQL Server",
}

type DBKeys = `I prefer using ${DB[keyof DB]}`;
// DBKeys = "I prefer using MySQL" | "I prefer using PostgreSQL" | "I prefer using SQLite" | "I prefer using Microsoft SQL Server"

索引访问类型

我们可以使用索引访问来查找具体一个类型的属性:

1
2
type Person = { age: number; name: string };
type Age = Person["age"]; // number

我们还可以使用任意类型的索引,比如number用来获取数组元素的类型,可以将它与typeof结合起来:

1
2
3
4
5
6
7
8
9
10
11
const Persons = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];

type Person = typeof Persons[number];
// type Person = {
// name: string;
// age: number;
// }

TS自带类型解析

TypeScript中自带了很多工具类型,这些工具类型算是类型体操的鼻祖。

Partial

Partial可以让类型里的属性变成可选的。

通过keyof​和in​,在每一个属性后加上?​,使属性变成可选的:

1
2
3
type MyPartial<T> = {
[P in keyof T]?: T[P]
}

Required

Required可以让类型里的属性都变成必填的,和Partial相反。

和Partial一样,只需为每一个属性移除?修饰符(-?​),就可以让属性变成必填的:

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

Readonly

Readonly可以让类型每一个属性变得只读。

和前面的一样,只需为每一个属性添加上readonly​标识符即可

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

Pick<T,K>

Pick可以中T挑选出K中的属性,返回新的类型,因此K必须是T的键名联合:

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

Record<K,T>

Record可以创建类型K的键和类型T的值的对象,因此K应该是任意的类型。

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

Exclude<T,U>

Exclude可以从联合类型T中,排除其中的类型U。这里要用到extends​,判断 T是否可以赋值给U,如果可以就返回never​,否则返回T:

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

Extract<T,U>

和Exclude相反,Extract从联合类型中,选出其中的类型U。

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

Omit<T,K>

Omit和Pick相反,从T中省略掉K中的属性,返回一个新的类型,这里可以使用Exclude,和Pick结合,先从typeof T​中Exclude掉K中的属性,然后再Pick返回的属性。

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

Parameters

Parameters用于获取一个函数参数的类型,因此,这里接受的T必须是一个函数,我们只需要拿到这个函数的参数类型即可,这里需要用到infer​,对参数的类型进行推导

1
2
3
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer U) => any
? U
: never;

Awaited

Awaited类型接收一个类Promise的类型,并且返回这个Promise的结果的类型。首先判断入参的类型是不是类Promise,这里可以用PromiseLike来判断,判断时推导出Promise的结果的类型,由于Promise是可以链式调用的,返回的结果也可以是Promise,所以要再判断一下,如果结果的类型是Promise的话,还需要递归一次,否则直接返回就好了。

1
2
3
4
5
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer P>
? P extends PromiseLike<any>
? MyAwaited<P>
: P
: T;

Easy

现在开始来实现type-challenges中的Easy级别的题目,这里不包含那些自带工具类型的实现。

First of Array

获取数组的第一个元素,可以先判断数组是不是空数组,如果是就返回never​,不是就返回第一个元素就好了。

1
type First<T extends any[]> = T extends [] ? never : T[0];

Length of Tuple

获取数组的长度,这里需要注意的是,需要对类型使用readonly​,使用readonly​修饰符可以确保数组的长度是不变的,只有长度不变的数组在编译时才能获取到具体的长度,否则获取到的是number​。

1
type Length<T extends readonly any[]> = T['length'];

If

接收三个参数,实现类似三元表达式的效果,使用extends​判断第一个参数是不是为true​即可。

1
type If<C extends boolean, T, F> = C extends true ? T : F;
​​

Concat

实现Concat,拼接两个数组,使用解构运算符即可。

1
type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U];
​​​

Include

实现Array.include​方法,需要用到递归的方法,从数组中推导出第一个元素和剩下元素的数组,判断第一个元素与包含的元素相等,是就返回true​,否则就递归判断剩下的元素的数组。

1
2
3
4
5
type Includes<T extends any[], U> = T extends [infer FIRST, ...infer REST]
? Equal<U, FIRST> extends true
? true
: Includes<REST, U>
: false;
​​​​

Push

Array.Push​方法,使用解构运算符即可:

1
type Push<T extends any[], U> = [...T, U];
​​​​​