类型体操是 TypeScript 中一种强大的类型编程方式,能够在类型层面实现复杂逻辑。它依赖 TypeScript 的条件类型、映射类型、推导类型和递归类型等特性,广泛用于动态类型推导、类型转换和高级类型安全逻辑的实现
在 TypeScript 类型体操中,if 和 for 的实现主要依赖于条件类型(Conditional Types)、递归类型(Recursive Types)以及元组操作。
实现 if 逻辑
TypeScript 中的条件类型类似于 JavaScript 的三元运算符 condition ? trueValue : falseValue。在类型层面实现 if,依赖于条件类型:
type If<Condition, TrueType, FalseType> = Condition extends true
? TrueType
: FalseType;
如下代码使用示例:
type IsTrue = If<true, "Yes", "No">; // 'Yes'
type IsFalse = If<false, "Yes", "No">; // 'No'
条件可以不仅仅是布尔值,我们还可以通过分配性条件类型判断更多复杂逻辑。例如:
type If<Condition extends boolean, TrueType, FalseType> = Condition extends true
? TrueType
: FalseType;
type IsString<T> = If<T extends string ? true : false, "String", "Not String">;
最终使用效果如下图所示:
实现 for 循环
在 TypeScript 类型系统中,没有直接的循环机制。因此,for 循环通常通过递归类型的方式实现。递归的思想是,将操作应用在一组类型上,不断减少问题规模,直到满足终止条件。
基本实现
最基本的递归模式如下:
type Loop<
T extends number,
Result extends any[] = []
> = Result["length"] extends T ? Result : Loop<T, [...Result, 1]>;
使用示例如吓图所示:
在上面的这个代码示例中展示了使用数组长度作为计算器,通过递归来累积结果,并使用条件类型来判断终止条件。
遍历数组
遍历数组是我们最常见的循环操作之一:
type MapArray<
Arr extends any[],
Handler extends (item: any) => any
> = Arr extends [infer First, ...infer Rest]
? [ReturnType<Handler>, ...MapArray<Rest, Handler>]
: ReturnType<Handler>[];
// 使用示例
type StringArray = MapArray<[1, 2, 3], (n: number) => string>; // [string, string, string]
MapArray 利用 TypeScript 的条件类型和递归,将数组类型逐个拆分为首个元素 First 和剩余元素 Rest。通过对 First 应用处理函数 Handler 的返回类型,并递归处理 Rest,逐步构造出一个新的数组类型。最终,当数组为空时,递归终止并返回空数组类型,模拟了 Array.map 的逻辑但作用于类型层面。
接下来我们来使用一个具体例子来展示如何使用 MapArray 来实现 for 循环:
type MapArray<
Arr extends any[],
Handler extends (item: any) => any
> = Arr extends [infer First, ...infer Rest]
? [ReturnType<Handler>, ...MapArray<Rest, Handler>]
: ReturnType<Handler>[];
type NumberToString = (n: number) => string;
// 实现一个将数字数组转换为字符串数组的函数
function mapNumbersToStrings(
arr: number[]
): MapArray<typeof arr, NumberToString> {
return arr.map((num) => String(num));
}
// 调用函数
const result = mapNumbersToStrings([1, 2, 3]);
console.log(result); // ["1", "2", "3"]
这段代码通过定义类型 MapArray 实现了对数组中每个元素的类型转换,并将转换逻辑应用于函数 mapNumbersToStrings 中,返回一个类型安全的结果。类型 StringArray 是一个元组类型 [string, string, string],表示将 [1, 2, 3] 转换为对应的字符串数组。函数中使用类型推导和断言确保返回值与类型定义一致,同时可以直接使用 StringArray 类型声明变量,保证代码的可读性和复用性。
数组累加
接下来我们应该上点难度,来实现一个数组求和的函数:
// 构造一个长度为 N 的数组
type BuildArray<
N extends number,
Arr extends any[] = []
> = Arr["length"] extends N ? Arr : BuildArray<N, [...Arr, 1]>;
// 辅助类型:加法
type AddNumbers<A extends number, B extends number> = [
...BuildArray<A>,
...BuildArray<B>
]["length"] extends number
? [...BuildArray<A>, ...BuildArray<B>]["length"]
: never;
// 将数组中的所有数字相加
type Sum<Arr extends number[], Result extends number = 0> = Arr extends [
infer First extends number,
...infer Rest extends number[]
]
? Sum<Rest, AddNumbers<Result, First>>
: Result;
// 使用示例
type Result = Sum<[1, 2, 3]>; // 6
接下来我们将详细捋一下这里代码的实现逻辑;
BuildArray 构造数组它的作用是通过递归构造一个长度为 N 的数组,利用数组的 length 属性模拟数字,用数组长度表示数字的大小。
具体的实现逻辑是分为如下步骤:
-
起始状态:Arr 默认是空数组 []。
-
递归终止条件:Arr["length"] 等于 N 时,返回数组 Arr。
-
递归调用:如果 Arr["length"] 不等于 N,则将 1 添加到 Arr 中并继续调用。
如下代码所示:
type ThreeArray = BuildArray<3>; // 等价于 [1, 1, 1],长度为 3
type FiveArray = BuildArray<5>; // 等价于 [1, 1, 1, 1, 1],长度为 5
而 AddNumbers 则是通过递归构造两个数组,然后通过数组长度来模拟加法运算。它的作用是将两个数字 A 和 B 转换为数组,通过数组的拼接模拟加法运算,拼接两个数组后,获取新数组的长度,作为加法运算的结果。
它的具体实现逻辑如下:
-
BuildArray<A>和BuildArray<B>分别生成长度为 A 和 B 的数组。 -
使用
[...BuildArray<A>, ...BuildArray<B>]拼接这两个数组。 -
拼接后的数组长度(
["length"])就是A + B的结果。
如下代码所示:
type Sum3And5 = AddNumbers<3, 5>; // 等价于 [1, 1, 1, 1, 1, 1, 1, 1]["length"],结果为 8
type Sum2And4 = AddNumbers<2, 4>; // 等价于 [1, 1, 1, 1, 1, 1]["length"],结果为 6
最后我们来看一下 Sum 的实现,Sum 通过递归遍历数组,它的作用是通过递归解构数组,将数组中的每个元素与 Result 相加,最终返回数组中所有元素的和。
它的具体实现逻辑如下:
-
分解数组:
-
使用条件类型
Arr extends [infer First, ...infer Rest]拆分数组,将第一个元素提取为First,其余元素为Rest。 -
如果数组为空(
Arr为[]),返回累加结果Result。
-
-
递归累加:对于数组 Arr = [1, 2, 3]:
-
第一次递归:
First = 1, Rest = [2, 3],调用Sum<Rest, AddNumbers<Result, First>>,即Sum<[2, 3], AddNumbers<0, 1>>。 -
第二次递归:
First = 2, Rest = [3],调用Sum<Rest, AddNumbers<Result, First>>,即Sum<[3], AddNumbers<1, 2>>。 -
第三次递归:
First = 3, Rest = [],调用Sum<Rest, AddNumbers<Result, First>>,即Sum<[], AddNumbers<3, 3>>。
-
-
递归终止:当数组为空时(
Arr = []),返回最终的Result值。
最终如下代码所示:
type Result1 = Sum<[1, 2, 3]>; // 等价于 AddNumbers<AddNumbers<AddNumbers<0, 1>, 2>, 3>,结果为 6
type Result2 = Sum<[4, 5, 6]>; // 等价于 AddNumbers<AddNumbers<AddNumbers<0, 4>, 5>, 6>,结果为 15
最终实现的原理总结:
-
通过数组长度模拟数字:使用
BuildArray<N>构造长度为N的数组,通过["length"]获取数字值。 -
通过数组拼接实现加法:
AddNumbers<A, B>拼接两个数组,并通过结果数组的长度模拟加法运算。 -
递归实现累加:
Sum<Arr>通过递归拆解数组,将每个元素与当前结果Result累加,最终返回累加值。 -
静态类型计算:所有计算都是在类型层面完成的,结果由 TypeScript 编译器在编译时静态推导出来。
这段代码通过巧妙地利用 TypeScript 的类型系统和递归,实现在类型层面计算数组元素的和。
中断循环
在类型体操中,我们可以使用条件类型来"中断"循环:
// 查找数组中的元素
type FindIndex<
Arr extends any[],
Item,
Counter extends any[] = []
> = Arr extends [infer First, ...infer Rest]
? First extends Item
? Counter["length"]
: FindIndex<Rest, Item, [...Counter, 1]>
: -1;
// 使用示例
type Result = FindIndex<[1, 2, 3], 2>; // 1
这段代码通过递归拆分数组来实现对元素索引的查找:
-
拆分数组:
Arr extends [infer First, ...infer Rest]将数组拆分为第一个元素First和剩余部分Rest,每次递归处理一个元素。 -
匹配目标元素:如果
First等于目标元素Item,返回当前计数器Counter的长度(Counter['length']即索引)。 -
累加计数器:如果
First不匹配目标元素,递归处理剩余数组Rest,并在计数器Counter中添加一个1来模拟索引递增。 -
递归终止:当数组为空(
Arr为[]),返回-1表示未找到目标元素。
通过这个逻辑,递归遍历数组并返回目标元素的索引,或返回 -1 表示未找到。
for 循环和 if 判断混用
在 TypeScript 类型体操中,for 循环和 if 判断可以混用,来实现更复杂的逻辑。
数组过滤
加下来我们来实现一个过滤数组:
// 过滤数组中的指定类型
type FilterArray<Arr extends any[], FilterType> = Arr extends [
infer First,
...infer Rest
]
? First extends FilterType
? [First, ...FilterArray<Rest, FilterType>] // if true: 保留元素
: FilterArray<Rest, FilterType> // if false: 跳过元素
: [];
// 使用示例
type NumbersOnly = FilterArray<[1, "a", 2, "b", 3], number>; // [1, 2, 3]
type StringsOnly = FilterArray<[1, "a", 2, "b", 3], string>; // ['a', 'b']
如下结果所示:
查找满足条件的第一个元素
type FindFirst<Arr extends any[], Condition> = Arr extends [
infer First,
...infer Rest
]
? First extends Condition
? First // if true: 返回找到的元素
: FindFirst<Rest, Condition> // if false: 继续查找
: never;
// 使用示例
type Found = FindFirst<[1, "hello", true, 2], string>; // 'hello'
如下结果所示:
计数满足条件的元素
type CountIf<
Arr extends any[],
Condition,
Count extends any[] = []
> = Arr extends [infer First, ...infer Rest]
? First extends Condition
? CountIf<Rest, Condition, [...Count, 1]> // if true: 计数加1
: CountIf<Rest, Condition, Count> // if false: 计数不变
: Count["length"];
// 使用示例
type NumberCount = CountIf<[1, "a", 2, "b", 3], number>; // 3
如下结果所示:
分组数组元素
type Partition<
Arr extends any[],
Condition,
Truthy extends any[] = [],
Falsy extends any[] = []
> = Arr extends [infer First, ...infer Rest]
? First extends Condition
? Partition<Rest, Condition, [...Truthy, First], Falsy> // if true
: Partition<Rest, Condition, Truthy, [...Falsy, First]> // if false
: [Truthy, Falsy];
// 使用示例
type Split = Partition<[1, "a", 2, "b", 3], number>; // [[1, 2, 3], ['a', 'b']]
如下结果所示:
条件替换数组元素
type ReplaceIf<Arr extends any[], Condition, Replacement> = Arr extends [
infer First,
...infer Rest
]
? First extends Condition
? [Replacement, ...ReplaceIf<Rest, Condition, Replacement>] // if true: 替换
: [First, ...ReplaceIf<Rest, Condition, Replacement>] // if false: 保持
: [];
// 使用示例
type Replaced = ReplaceIf<[1, "a", 2, "b", 3], number, "num">;
// ['num', 'a', 'num', 'b', 'num']
如下图示例所示:
这些例子展示了如何在类型体操中组合使用循环(递归)和条件判断。关键点是:
-
使用
extends和infer来进行模式匹配和类型推断 -
使用条件类型
(?:)来实现分支逻辑 -
使用递归来实现循环
-
使用辅助数组来进行计数或累积结果
这些模式可以组合使用来实现更复杂的类型运算。记住要始终提供终止条件以避免无限递归。
总结
if 的实现依赖于 TypeScript 的条件类型,可以用于基于布尔值或复杂逻辑实现分支处理。for 则通过递归类型和元组操作来模拟循环逻辑,支持对数组元素或固定次数的操作。结合 if 和 for,可以实现基于条件的复杂循环逻辑,从而完成更灵活的类型处理。
最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:
如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。