关于Ts类型约束为 数字字面量 范围的尝试
最近在小册里深入学习Ts的过程中,发现了这个一个问题:
如果要定义一个比较精准的number范围类型,需要怎么定义,比如我需要一个number的值在4-400的范围之间
虽然在这个问题的下面就有大佬给出了回答,如下:
Enumerate类型是一个递归类型,它用来创建一个长度为 N 的数字数组。它的第一个参数 N 是数字类型,表示数组的长度。它的第二个参数 Acc 是数字数组类型,表示当前递归中数组的值。当 Acc 的长度等于 N 时,这个递归类型会停止递归,并返回 Acc 数组中的每一项。如果 Acc 的长度小于 N,那么递归类型会将当前的长度添加到 Acc 数组中,并继续递归。IntRange 是一个由两个数字类型参数 F 和 T 组成的类型,它表示从 F 到 T 的一个整数范围。它通过使用 Exclude 类型来排除由 Enumerate 类型生成的数组中的所有小于 F 的元素,从而实现F到T。
type Enumerate<N extends number, Acc extends number[] = []> = Acc["length"] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc["length"]]>;
type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
type T = IntRange<4, 400>;
let value: T = 40 // OK
关于这段代码确实能实现范围约束,分解如下:
type MinRange = Enumerate<4>; // 0 | 1 | 2 | 3
type MaxRange = Enumerate<400>; // 0 | 1 | .... |399
type T = Exclude<MaxRange, MinRange> // 4 | 5 | ... |399
可以看到确实实现了把 小于4 的部分从大范围 Enumerate<400> 中剔除了,但这有一个问题就是 Enumerate<400> 本身生产的类型是 小于399 的,也正如注释所写,最终的类型实际上缺少了 400 这一项。
let value: T = 400 // 不能将类型“400”分配给类型“T”
正好最近在写类型体操,于是自己琢磨怎么能把这个 400 给补上
一、 最简单粗暴的,修改入参
最简单的方法,不是右开区间吗,那我直接传参的时候多加 1 就行了,如下:
- type T = IntRange<4, 400>;
+ type T = IntRange<4, 401>;
let value: T = 400 // OK
二、 修改IntRange
觉得直接修改参数不太优雅,那我们可以把 +1 的这个逻辑放到 IntRange 类型工具里面去,也就是把 Enumerate<T> 这里面的泛型 T +1,但想在字面量类型上+1肯定不能简简单单的直接写成 Enumerate<T + 1>,这就需要用到 Add 类型工具,具体实现可以参考 三水青禾 大佬的这篇文章 玩转TS类型体操,在这里我直接给出具体定义
type MakeTuple<
Target extends number,
CurTuple extends any[] = []
> = CurTuple["length"] extends Target ? CurTuple : MakeTuple<Target, [...CurTuple, any]>;
// 两数相加
type Add<T1 extends number, T2 extends number> = [
...MakeTuple<T1>,
...MakeTuple<T2>
]["length"];
type res1 = Add<1, 2>; // 3
改写如下:
- type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
+ type IntRange<F extends number, T extends number> = Exclude<Enumerate<Add<T,1> & number>, Enumerate<F>>;
三、 自己尝试着写一个新的
当然,除了在大佬的基础上做修改,我也有一些自己的思考来完成这样一个类型工具,具体思路如下:
- 基本定义:对于整个工具,我有如下基本定义框架,接受
Min和Max两个泛型分别代表左端值和右端值
type Limit<Min extends number, Max extends number> = Min | .... | Max
type res1 = Limit<1, 1>; // 1
type res2 = Limit<2, 1>; // never
type res3 = Limit<1, 5>; // 1 | 2 | 3 | 4 |5
- 构造两个长度分别为
Min、Max的元组类型和一个 递归类型工具,这个工具接受两个 any[] 类型的泛型,在每次进入时,比较两个元组的长度- 若 Min元组 长度> Max元组 则返回 never
- 若 Min元组 长度<= Max元组 则将 Max元组 的当前长度字面量并入总联合类型,然后删去 Max元组 的最后一个元素使它长度-1,并重新将两个元组传入自身递归调用
type Recurrence<TupleA extends any[], TupleB extends any[]> = Compare<TupleA["length"], TupleB["length"]> extends
| 1
| 0
? TupleB["length"] | Recurrence<TupleA, SliceLast<TupleB>>
: never;
type Limit<Min extends number, Max extends number> = Recurrence<
MakeTuple<Min>,
MakeTuple<Max>
>;
在上述模板中,我们用到了额外的几个类型工具
MakeTuple构造一个大小为传入的 number 字面量的元组,这在第二个方法中已经出现过SliceLast删除元组最后一个元素,并返回删除后的元组Compare比较两个 number 字面量的大小- A > B 返回 -1
- A = B 返回 0
- A < B 返回 1
这三个类型工具都算比较常见,所以我直接给出最终整合版:
// 构造一个 长 Target 的元组类型
type MakeTuple<
Target extends number,
CurTuple extends any[] = []
> = CurTuple["length"] extends Target ? CurTuple : MakeTuple<Target, [...CurTuple, any]>;
// 删除元组类型最后一个元素,返回删除后的元组类型
type SliceLast<T extends any[]> = T extends [...infer Rest, any] ? [...Rest] : [];
// Compare递归工具
type CompareTuple<TA extends any[], TB extends any[]> = IsEqualLen<TA, TB> extends false
? TA["length"] extends 0
? 1
: TB["length"] extends 0
? -1
: CompareTuple<SliceLast<TA>, SliceLast<TB>>
: 0;
// 比较两数大小
type Compare<T1 extends number, T2 extends number> = CompareTuple<
MakeTuple<T1>,
MakeTuple<T2>
>;
// Limit递归工具
type Recurrence<TupleA extends any[], TupleB extends any[]> = Compare<TupleA["length"], TupleB["length"]> extends
| 1
| 0
? TupleB["length"] | Recurrence<TupleA, SliceLast<TupleB>>
: never;
// 构造number范围的联合类型
type Limit<Min extends number, Max extends number> = Recurrence<
MakeTuple<Min>,
MakeTuple<Max>
>;
type res1 = Limit<1, 1>; // 1
type res2 = Limit<2, 1>; // never
type res3 = Limit<1, 5>; // 1 | 2 | 3 | 4 |5
至此我自己的想法实现完成,也算完成了目标。
当然以上实现肯定是写复杂了,事实上我觉得在原本的基础上稍作改动就好,我这个算舍近求远,但写这个的初衷在于对自己近期所学尽量都用一下,所以写出了这么一篇水文,还望大家多多指点!谢谢!