Typescript - 整型Range类型
2021-08-18
Coding
StackOverflow
Typescript
👋 ‍️‍️阅读
❤️ 喜欢
💬 评论

Typescript - 整型Range类型

问题

原题链接

原问题很简单,如何在Typescript中定义一个联合特定范围数字的整型类型。

Accepted Answer给出的答案是:

No it's not possible. That kind of precise type constraint is not available in typescript (yet?)
Only runtime checks/assertions can achieve that :(

然而,该问题创建于2016年9月14日,当时Typescript v2.0版本尚未发布(9月22日)。

这一需求在2017就有Issue在讨论,到今天依然是Open状态,或许在未来的某个版本会得到原生支持。

时至今日,Typescript v4.3已经发布了。那么利用Typescript的新特性能否完成这个功能呢? 就让我们来尝试一下。

趣事

解题之前,说个搞笑的事情。

在这个问题提出并接受了答案的半年之后。 一位老兄给出了一个显然没有仔细审题也没有思考的答案。

当然,这可以正常工作。但是对于题主的需求,显然不会有人想在代码里硬编码从0到255。 题主也在题目中说明了他不想写下256个数字。

然而,这样一个显然不符合题设的回答居然获得81个Upvote,甚至超过了被接受答案的48票。 同时,因为这显然是错误答案,他也收获了11个Downvote。

该回答的评论中点破答主没有好好审题的老哥,通过一句简单的引用得到了76个Upvote,成为了这一页当中总分最高的选手🤣

求解

我们最终要得到的一个类型

type Range<T extends number> = ?

在Typescript中,整型字面量可以作为类型使用。但是却不能进行运算。 唯一可以进行的运算就是等于操作,可以利用extends条件类型

type Equals<A extends number, B extends number> = A extends B? true: false
type T1 = Equals<2, 2> // true
type T2 = Equals<2, 3> // false

所以问题的关键是,如何通过T衍生出其他数字?即给定整形字面量类型A,如何得到A+1A-1

我的思路是通过元组长度,元组类型的length属性会得到它的长度,这个长度是一个准确的整型字面量类型。

type A = [1, 2, 3]
type B = A['length'] // 3

而新的问题就是,如何得到一个长度为T的元组类型?

我们可以通过递归条件类型,从一个空数组开始构造

type _Tuple<N extends number, T extends unknown[]> = N extends T['length'] ? T : _Tuple<N, [...T, N]>
type Tuple<N extends number> = _Tuple<N, []>
type T1 = Tuple<5> // [5, 5, 5, 5, 5]

对于长度N和元组T,如果长度满足则返回T,否则给T加一个元素并重新判断。 考虑到我们总是从空数组[]开始,所以提炼了一个助手类型_Tuple

有了这个思路,我们可以依葫芦画瓢,就得到类似的Plus1Sub1类型:

type _Plus1<N extends number, T extends unknown[]> = N extends T['length'] ? [...T, N]['length'] : _Plus1<N, [...T, N]>
type Plus1<N extends number> = _Plus1<N, []>
type T2 = Plus1<5> // 6

type _Sub1<N extends number, T extends unknown[]> = N extends T['length'] ?
    (T extends [...(infer S), unknown] ? S['length'] : never)
    : _Sub1<N, [...T, N]>
type Sub1<N extends number> = _Sub1<N, []>
type T3 = Sub1<5> // 4

现在我们有了加减法,通过它们就可以得到最终结果,只需要在递归时联合类型即可

type _Ran<N extends number, T extends unknown[]> = N extends T['length'] ? never : T['length'] | _Ran<N, [...T, N]>
type Ran<N extends number> = _Ran<N, []>

type T4 = Ran<5> // 0 | 1 | 2 | 3 | 4

这样就是该问题的解答了。

但是由于递归层数的限制,这个类型不能处理很大的数字,例如题目中的256。 在v4.3.5的环境下,最多只能递归到22。即Ran<23>就会报错。

Typescript Playground链接在此

在这个解法的基础上进行了一些简化和保护,就得到了我的答案

另外,比我早一年,有一个网友也给出了类似的解法。他使用了方法入参进行推导,和元组异曲同工。


Copyright © 2020-2022 Dean Xu. All Rights reserved.