Skip to content

TypeScript 类型编程小技巧

TypeScript 可以通过类型编程去灵活生成我们想要的类型。下面我们就来讲讲其中的一些小技巧吧。

三种类型系统

首先,我们先来讲讲类型系统。

简单类型系统

最基础的类型系统,保证了类型安全,但只有最基础的类型设置,类型灵活性比较低。

支持泛型的类型系统

进阶一点的,其实就是我们支持泛型的类型系统,我们可以通过我们泛型系统和指定的参数去生成我们指定的类型,增加了类型的灵活性。

泛型提供了编译时类型安全检测机制,该机制允许开发在编译时检测到非法的类型。 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

支持类型编程的类型系统

支持对传入的类型参数做逻辑运算,并且能够产生新类型的类型系统,这种操作也就是字面意义的类型编程,这大大提高了类型系统的灵活性。

类型编程?类型体操?

TypeScript很明显就是支持类型编程的类型系统,类型编程提供了高度的灵活性。

TypeScript是图灵完备的,我们能够用 TS 所提供的语法以及基础工具,去进行条件判断,递归,类型推断等操作去,同时我们进行复杂的组合计算,获得新的类型,从而可以去实现Pick等内置泛型工具, 斐波那契数列,中国象棋,Lisp解释器,HypeScript类型系统,这也被称为类型体操

类型编程的小技巧

前置知识

在接触类型编程/体操,我们需要对TypeScript的基础有一定了解和熟悉,这能让我们更加好的理解。

条件类型

extends的写法,有点类似于三目运算符。

简单理解:如果T包含的类型 是 U包含的类型的 '子集',那么取结果X,否则取结果Y

typescript

复制代码

T extends U ? X : Y

infer推断

infer, 能够推断出变量的类型,但是,只能在条件语句extends下进行使用。

typescript

复制代码

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

元组操作

元祖我们可以理解为定长、定类型的数组。

typescript

复制代码

type Tunple = [1, 'string', false];

元组的核心在于...infer的结合。

typescript

复制代码

type concat<A extends any[], B extends any[]> = [...A, ...B]; type GetFirst<T extends any[]> = T extends [infer First, ...infer any[]] ? First : never;

泛型工具

TypeScript中,有内置一些泛型工具,提供我们做类型转换。

这里的话就不加多介绍了, 可以查阅 TS 的文档Documentation - Utility Types

PartialRequiredReadonlyRecord<Keys, Type>Pick<Type, Keys>Omit<Type, Keys>Exclude<UnionType, ExcludedMembers>
Extract<Type, Union>NonNullableParametersConstructorParametersReturnTypeInstanceTypeThisParameterType
OmitThisParameterThisTypeUppercaseLowercaseCapitalizeUncapitalize

当然, TypeScript的基础当然不止这么多,还有索引类型,as等。

模式匹配

想象一个场景,如果我们想提取元组的最后一个元素的类型。

这个时候,其实我们可以借助infer这个工具来满足我们的需求。

GetLast

实现一个类型,用于提取元组的最后一个元素类型。

typescript

复制代码

type GetLast<T extends unknown[]> = T extends [...any[], infer Last] ? Last : never;

GetFirst

实现一个类型,用于提取元组的第一个元素类型。

typescript

复制代码

type GetFirst<Arr extends unknow[]> = Arr extends [infer First, ...unknown[]] ? First : never;

StartsWith

判断字符串是否以某个前缀开头。

typescript

复制代码

type StartsWith< Str extends string, Prefix extends string > = Prefix extends '' ? true : Str extends `${Prefix}${string}` ? true : false;

GetParameters

实现一个 Parameters 泛型工具。

typescript

复制代码

type GetParameters<Func extends Function> = Func extends ( ...args: infer Parameters ) => any ? Parameters : never;

模式匹配可以用在数组、字符串、函数等,实际上是我们去为类型构建对应的条件,从而利用extendsinfer两个基础工具,去对我们产生的新类型进行构造,伪代码表示。

重新构造

注意,我们都知道 TS 中的类型不会像我们的变量那样支持重新赋值的,即我们用type,infer,泛型参数都是唯一确定的,无法修改的,这个时候我们要产生新的类型就要对类型去进行修改。

AppendArgument

实现一个函数,对函数类型的,往函数类型里面添加新的类型参数。

typescript

复制代码

type AppendArgument<Fun extends Function, Ele> = Fun extends (...args: infer OriginArgs) => any ? (...args: [Ele, ...OriginArgs]) => any : void;

ReplaceStr

我们实现字符串类型中的指定字符的替换。

typescript

复制代码

type ReplaceStr< Str extends string, From extends string, To extends string > = Str extends `${infer Prefix}${From}${infer Suffix}` ? `${Prefix}${To}${Suffix}` : Str;

ParitalByKeys

实现一个根据Key值过滤的 Parital。

typescript

复制代码

type PartialByKeys<T, K = keyof T> = { [P in keyof T as P extends K ? never : P]: T[P] } & { [P in keyof T as P extends K ? P : never]?: T[P] } extends infer A ? { [P in keyof A]: A[P] } : never;

重新构造的点也在于,我们如何提取,以及如何构造伪代码表示:

递归操作

TS中是支持我们去做递归计算的,不过最好结合extendsinfer

递归的基本要素

基线条件:确定递归到何时终止,函数不再调用自己,也称为递归出口; 递归条件:函数调用自己,将大问题分解为类似的小问题,也称为递归体。

DeepAwaited

实现一个嵌套Promise的提取。

typescript

复制代码

type DeepAwaited<T> = T extends Promise<infer R> ? R extends Promise<infer P> ? DeepAwaited<P> : R : T; type Test = DeepAwaited<Promise<Promise<Promise<Promise<Promise<number>>>>>>

ReplaceAll

之前实现了Reaplce, 当时只支持了一次匹配替换,接下来,我们可以在原来的基础上加上递归操作。

typescript

复制代码

type ReplaceAll< Str extends string, From extends string, To extends string > = Str extends `${infer Prefix}${From}${infer Suffix}` ? `${Prefix}${To}${ReplaceAll<Suffix, From, To>}` : Str;

Reverse

实现一个类型,类似于Array.reverse

typescript

复制代码

type Reverse<T extends any[]> = T extends [...(infer Rest), infer Last] ? [Last, ...Reverse<Rest>] : [];

BuildArr

实现一个类型,构建数组。

typescript

复制代码

type BuildArr< Length extends number, Ele = unknown, Arr extends unknown[] = [] > = Arr['length'] extends Length ? Arr : BuildArr<Length, Ele, [...Arr, Ele]>;

递归操作注意递归出口+递归体伪代码表示:

类型计数

LengthOfString

实现一个类型,可以统计传入的字符串字面量的长度。

typescript

复制代码

type LengthOfString<S extends string, Result extends string[] = []> = S extends `${infer First}${infer Next}` ? LengthOfString<Next, [...Result, First]> : Result['length'];

Add

实现一个类型加法。

typescript

复制代码

type Add<num1 extends number, num2 extends number> = [ ...BuildArr<num1>, ...BuildArr<num2>, ]['length'];

数值一般是对数组进行操作,并提取他的length属性, 伪代码表示。

类型编程的意义

  • 技术上类型理解

  • 业务开发中的规范

  • 类型编程?类型体操?

类型编程能帮助你更好地理解复杂类型编程的底层原理,同时类型编程可以通过类型运算产出更准确的类型,也能够让你获得独立解决各种类型问题的能力。

扩展

Lisp 解释器: TypeScript 类型体操天花板,用类型运算写一个 Lisp 解释器 - 掘金

中国象棋用 TypeScript 类型运算实现一个中国象棋程序

井字棋: TS 实现简易的井字棋 - 掘金

HypeScript: GitHub - ronami/HypeScript: 🐬 A simplified implementation of TypeScript's type system written in Typ

参考资料

Released under the MIT License.