秋逸

TypeScript 类型体操:两数求和

给定一个数组 arr 和一个数字 target,我们要判断能否从 arr 中取出两个数,通过加法得到 target。

结果展示

type ToTuple<
	L extends number,
	A extends Array<any> = [],
> = A['length'] extends L ? A : ToTuple<L, [...A, any]>

type Sub<A extends number, B extends number> =
	ToTuple<A> extends [...ToTuple<B>, ...infer Tail] ? Tail['length'] : -1

type FirstElement<A extends Array<any>> = A extends [infer F, ...infer Tail]
	? F
	: never

type TailElements<A extends Array<any>> = A extends [infer F, ...infer Tail]
	? Tail
	: never

type MapSub<A extends any[], B extends number> = A['length'] extends 0
	? []
	: [Sub<B, FirstElement<A>>, ...MapSub<TailElements<A>, B>]

type Include<A extends any[], B extends any> = A['length'] extends 0
	? false
	: FirstElement<A> extends B
		? true
		: Include<TailElements<A>, B>

type TwoSum<
	Arr extends Array<number>,
	Target extends number,
	Rest extends number[] = Arr,
> = Rest['length'] extends 0
	? false
	: Include<MapSub<Arr, Target>, FirstElement<Rest>> extends true
		? true
		: TwoSum<Arr, Target, TailElements<Rest>>

type res2 = Sub<10, 7> // 3
type res3 = FirstElement<[1, 2, 3]> // 1
type res4 = TailElements<[1, 2, 3]> // [2, 3]
type res6 = TwoSum<[1, 2, 3], 6> // true
type res7 = TwoSum<[1, 2, 3], 7> // false

思路

看不懂?没关系,先用编程语言实现一下类似的逻辑,找找思路:

function twoSum(arr: number[], target: number): boolean {
	return arr.some((el) => arr.includes(target - el))
}

很简单对吧,只要 arr 里存在 target - el 就返回 true。

那为什么写成类型就变得如此复杂?让我们一步一步来看!

首先,为了计算 target - el,我们需要实现一个减法类型 Sub

由于 TypeScript 中的加减法可以用数组的长度来间接得到,我们不得不写一个 ToTuple 来把数字转化成对应长度的数组。

为了判断数组中是否存在 target - el,我们还需要完成一个 Include 类型。

实现

ToTuple

在 TypeScript 中,直接进行数字运算是不可能的,但我们可以通过数组的长度来模拟数字。因此,我们使用递归的方式构建一个长度为 L 的数组 A,当 A[‘length’] 达到 L 时,递归结束,返回数组 A。

type ToTuple<
	L extends number,
	A extends Array<any> = [],
> = A['length'] extends L ? A : ToTuple<L, [...A, any]>

Sub

我们利用 ToTuple 将数字 A 和 B 转换为相应长度的数组,然后通过模式匹配 ToTuple<A> 是否可以分解为 [...ToTuple<B>, ...infer Tail],从而确定 A - B 的结果为 Tail[‘length’]

type Sub<A extends number, B extends number> =
	ToTuple<A> extends [...ToTuple<B>, ...infer Tail] ? Tail['length'] : -1

FirstElement & TailElements

FirstElement 和 TailElements 分别用于提取数组的第一个元素和剩余元素。

通过模式匹配,FirstElement 提取数组的第一个元素,而 TailElements 则提取数组除第一个元素外的所有元素。

type FirstElement<A extends Array<any>> = A extends [infer F, ...infer Tail]
	? F
	: never

type TailElements<A extends Array<any>> = A extends [infer F, ...infer Tail]
	? Tail
	: never

MapSub

通过递归处理数组中的每个元素,使用 Sub 计算 B 与每个元素的差值,并将结果存储在新的数组中。之所以计算 B 与每个元素的差,而不是把每个元素都减去B,是因为在之后使用的时候,B 是 Target,大于数组中的任何一个元素,而Sub只能计算 A > B 的情况,因此只能计算 B 与每个元素的差。


type MapSub<A extends any[], B extends number> = A['length'] extends 0
	? []
	: [Sub<B, FirstElement<A>>, ...MapSub<TailElements<A>, B>]

Include

通过递归遍历数组,逐个比较元素,如果找到了目标元素则返回 true,否则返回 false。

type Include<A extends any[], B extends any> = A['length'] extends 0
	? false
	: FirstElement<A> extends B
		? true
		: Include<TailElements<A>, B>

最后,TwoSum

通过递归检查数组中的每个元素 el,计算 Target - el 的结果,并检查 Arr 中是否包含该结果。如果找到匹配对,则返回 true,否则继续递归检查。

type TwoSum<
	Arr extends Array<number>,
	Target extends number,
	Rest extends number[] = Arr,
> = Rest['length'] extends 0
	? false
	: Include<MapSub<Arr, Target>, FirstElement<Rest>> extends true
		? true
		: TwoSum<Arr, Target, TailElements<Rest>>
TypeScript 类型体操:两数求和
: 杨秋逸
https://yangqiuyi.com/blog/开发/typescript/typescript类型体操两数求和/
© 2025 杨秋逸 . All rights reserved.