一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
今天在学习TypeScript的时候,被一道看似简单的类型题难到了,题面很简短
Given a number (always positive) as a type. Your type should return the number decreased by one.
大意:给你的类型一个正整数,你的类型要返回这个正整数减一。
For example:
type Zero = MinusOne<1> // 0
type FiftyFour = MinusOne<55> // 54
很明显,这个类型就是将传入的正整数类型变为这个正整数 - 1。是不是感觉很简单,我们按直觉写写试试:
type MinusOne<T extends number> = T - 1;
不出所料,并没有这么简单,在减号上面飘红了,看来是不能直接减法的
那么认真思考下,应该怎么去实现这个类型呢?
一个直观的想法
我们去迭代生成一个元组,如果元组的长度等于传入的类型,那么,我们只要去掉开头,剩下的元组的长度就是减一的结果!还是很简单的嘛
type NumberToArr<T extends number, A extends number[] = []> = A['length'] extends T ?
A :
NumberToArr<T, [number, ...A]>
type MinusOne<T extends number> = NumberToArr<T> extends [infer N, ...infer R] ?
R['length'] :
never
果然没有想得这么简单,当T传入1001时,编译器会认为这个类型可能无限递归了,然后报错
很显然要用其他方法,减少递归次数
如果是元组
因为我们要考虑将最后一位减一,如果是元组的话,我们可以很简单的获得到最后一个类型,所以我们先考虑元组怎么实现,然后再写两个转换的type就好了。
减1的步骤:
- 首先将最最后一位减一
- 如果最后本身已经是零,那么变成九,并且前面的数字进行减一操作(递归)
- 否则,最后一位减一和前面拼接
按照这个思路,我们写下这个type
//首先建立最后一位减一后的结果映射
type MinusOneMap = { "0": "9", "1": "0", "2": "1", "3": "2", "4": "3", "5": "4", "6": "5", "7": "6", "8": "7", "9": "8" }
type TupleMinusOne<T extends unknown[]> =
//获得最后一个
T extends [...infer H, infer Last] ?
//如果最后是 0
Last extends '0' ?
H extends string[] ?
//前面的数字进行减一,然后拼个 9
[ //如果减完是 0,去除最高位
...TupleMinusOne<H> extends ['0'] ?
[] :
TupleMinusOne<H>,
MinusOneMap[Last]
] :
never :
//否则最后一位减一
Last extends keyof MinusOneMap ?
[...H, MinusOneMap[Last]] :
never :
never;
这样我们就实现了将元组形式的数字减一的type。
既然有了元组形式下的减一,那么我们只要再实现数字转化为字符串,字符串变为元组,减一后再变回数字就好了!
数字变为数字字符串
这个很简单,我们用模板字符串来转化就行。
type Number2String<T extends number> = `${T}`;
数字字符串变为元组
数字字符串变为元组其实就是每次接受一个字母放入,然后直到最后一个字母,也是递归实现即可。
type NumberString2Tuple<T> = T extends `${infer F}${infer R}` ?
[F,...NumberString2Tuple<R>] :
[];
那么我们就只剩下最后一步了,如何把减一后的元组再变回数字呢?
字符元组变为数字
元组 -> 数字,不难想到还是通过构造一个新的元组,通过length来进行转化,并且这个构造过程应该递归的尽可能前,过深肯定还是和之前一样报错。
首先考虑得到一串数字,我们怎么合成一个数字呢,步骤大家应该很熟悉,代码大概这样:
let nums = [.....];
let res = 0;
for(let n of nums){
res = res * 10 + n;
}
其实就是每碰到一个新数字,就把之前的结果乘10,然后再加上这个数字,按照这个思路,我们先实现把元组长度乘10的类型,其实思路很显然,用十次拓展运算符就好了。
type XTen<T extends unknown[]> = [
...T,...T,...T,...T,...T,...T,...T,...T,...T,...T
]
那么个位数怎么转化呢,其实可以借用解法一的思路
type NumberString2LengthTuple<T extends string,Res extends unknown[] = []> =
Number2String<Res['length']> extends T ?
Res:
Number2LengthTuple<T,[1,...Res]>
那么有了个位和乘10之后,为了方便,我们先将元组转为转为字符串
type TupleToString<T extends string[]> =
T extends [infer F,...infer R] ?
F extends string ?
R extends string[] ?
`${F}${TupleToString<R>}`:
"":
never:
""
最后按照上面说的单个数字合成一个数字的思路实现一个PraseInt:
type PraseInt<T extends string, Res extends unknown[] = []> = T extends `${infer Head}${infer Rest}` ?
//乘10 然后加 个位
PraseInt<Rest, [...XTen<Res>, ...NumberString2LengthTuple<Head>]> :
//最后返回长度
Res['length']
因为PraseInt每次处理1位数,所以几位数就递归几次,大大减少了递归次数。
最后整合起来:
type MinusOne<T extends number> =
PraseInt<TupleToString<TupleMinusOne<NumberString2Tuple<Number2String<T>>>>>
我们用一个用例检测下
通过了!
但是当数字太大时还是会报错,因为中间生成的元组太长了,从一种限制到了另一种限制,但是因为这个题最大用例就是1001,所以也算是勉强通过了吧,后面再想想其他的实现方法。
如果对你有帮助,不要忘记 点赞 和 关注 !
完整代码
最后附上完整代码
type MinusOneMap = { "0": "9", "1": "0", "2": "1", "3": "2", "4": "3", "5": "4", "6": "5", "7": "6", "8": "7", "9": "8" }
type TupleMinusOne<T extends unknown[]> =
//获得最后一个
T extends [...infer H, infer Last] ?
//如果最后是 0
Last extends '0' ?
H extends string[] ?
//前面的数字进行减一,然后拼个 9
[ //如果减完是 0,去除最高位
... TupleMinusOne<H> extends ['0'] ?
[] :
TupleMinusOne<H>,
MinusOneMap[Last]
] :
never :
//否则最后一位减一
Last extends keyof MinusOneMap ?
[...H, MinusOneMap[Last]] :
never :
never;
type Number2String<T extends number> = `${T}`;
type NumberString2Tuple<T> = T extends `${infer F}${infer R}` ?
[F,...NumberString2Tuple<R>] :
[];
type XTen<T extends unknown[]> = [
...T,...T,...T,...T,...T,...T,...T,...T,...T,...T
];
type NumberString2LengthTuple<T extends string,Res extends unknown[] = []> =
Number2String<Res['length']> extends T ?
Res:
NumberString2LengthTuple<T,[1,...Res]>;
type TupleToString<T extends string[]> = T extends [infer F,...infer R] ?
F extends string ?
R extends string[] ?
`${F}${TupleToString<R>}`:
"":
never:
"";
type PraseInt<T extends string, Res extends unknown[] = []> = T extends `${infer Head}${infer Rest}` ?
//乘10 然后加 个位
PraseInt<Rest, [...XTen<Res>, ...NumberString2LengthTuple<Head>]> :
//最后返回长度
Res['length']
type MinusOne<T extends number> = PraseInt<TupleToString<TupleMinusOne<NumberString2Tuple<Number2String<T>>>>>;