TypeScript里面计算 2 - 1 = 1为什么这么难?

1,006 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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;

不出所料,并没有这么简单,在减号上面飘红了,看来是不能直接减法的

d92bed77298b9a8b57381398999a089b3348ea02324e49b8af669f187d982fec.png

那么认真思考下,应该怎么去实现这个类型呢?

一个直观的想法

我们去迭代生成一个元组,如果元组的长度等于传入的类型,那么,我们只要去掉开头,剩下的元组的长度就是减一的结果!还是很简单的嘛

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时,编译器会认为这个类型可能无限递归了,然后报错

4f0eefdda7059755719b7bb3c4ac280ed1624363cecd9858f777d890697e48e9.png

很显然要用其他方法,减少递归次数

如果是元组

因为我们要考虑将最后一位减一,如果是元组的话,我们可以很简单的获得到最后一个类型,所以我们先考虑元组怎么实现,然后再写两个转换的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;

fe9a45dcc51b0694b30f805763557c5abe3b5eee58d0cd8dde81a1e6b82cba8a.png

这样我们就实现了将元组形式的数字减一的type。

既然有了元组形式下的减一,那么我们只要再实现数字转化为字符串,字符串变为元组,减一后再变回数字就好了!

数字变为数字字符串

这个很简单,我们用模板字符串来转化就行。

type Number2String<T extends number> = `${T}`; 

数字字符串变为元组

数字字符串变为元组其实就是每次接受一个字母放入,然后直到最后一个字母,也是递归实现即可。

type NumberString2Tuple<T> = T extends `${infer F}${infer R}` ?
    [F,...NumberString2Tuple<R>] :
    [];

50d8464d03b3f5c4fe4b563641ff4d0d7495d26cdd01c3587023a6b7573a9221.png

那么我们就只剩下最后一步了,如何把减一后的元组再变回数字呢?

字符元组变为数字

元组 -> 数字,不难想到还是通过构造一个新的元组,通过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>>>>>

我们用一个用例检测下

db37358bc5412f1092d050bfdb66f8df5fb5973648f952788e8283050a8901a4.png 通过了!

但是当数字太大时还是会报错,因为中间生成的元组太长了,从一种限制到了另一种限制,但是因为这个题最大用例就是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>>>>>;