TypeScript类型声明:4的倍数

2,518 阅读7分钟

如何在typeScript中定义仅接受4的倍数的数字类型

什么是TypeScript 类型声明

类型声明就是给变量设置了类型,使得变量只能存储某种类型的值

类型声明的作用:

  1. 通过类型声明可以指定 TS 中的变量(参数,形参)的类型
  2. 指定类型之后,再给变量赋值,会自动进行检测,如果符合则赋值,不符合则会抛出错误

TS 自带类型判断的功能:

当对变量的声明和赋值同时进行时,TS 编译器会自动判断变量的类型,因此当声明和赋值同时进行的时候,可以省略掉类型声明。

想进一步了解的小伙伴可以参考TypeScript 中文手册

对类型声明有一定了解后,我们继续研究如何才能在typeScript中定义仅接受4的倍数的类型?

可以直接将下方代码粘贴在TypeScript演练场

方法1

网上流传的一种方法如下:

type IsMultipleOfFour<T> = T extends number ? (T % 4 extends 0 ? T : never) : never;

function foo<T extends IsMultipleOfFour<T>>(value: T): void {
  // ...
}

foo(4); // OK
foo(8); // OK
foo(3); // Error: 类型“3”的参数不能赋给类型“IsMultipleOfFour<3>”的参数。

此方法看似合理,但是有报错:

'never' only refers to a type, but is being used as a value here.

经过一番度娘,Gpt... 重新修改定义:

type IsValidSize<T extends number, R extends number[] = []> = T extends R['length']
    ? R['length'] extends 0
        ? T
        : never
    : IsValidSize<T, [0, 0, 0, 0, ...R]>;

type IsMultipleOfFour<T extends number> = T extends IsValidSize<infer U> ? U : never;

type CheckIsMultipleOfFour<T extends number> = T extends IsMultipleOfFour<T> ? T : never;

function foo<T extends number>(value: CheckIsMultipleOfFour<T>): void {
    console.log(value)
}

解决了never的报错,尝试运行,又出现了新的报错,如下:

Type instantiation is excessively deep and possibly infinite.

原因:类型实例化过于深入且可能无限,即可能会导致递归深度过深的错误。

这种错误可能会导致编译器性能下降或内存耗尽,因此TypeScript会限制类型实例化的深度。当达到特定深度时,编译器将显示此错误。

避免递归深度过深:

type IsMultipleOfFour<T extends number> = T extends 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 96 | 100 ? T : never;

function foo<T extends number>(value: IsMultipleOfFour<T>): void {
    console.log(value)
}

foo(4); // OK
foo(8); // OK
foo(3); // Error: 类型“3”的参数不能赋给类型“IsMultipleOfFour<3>”的参数。

尝试运行...成功!

方法2

正则表达式(一种用于匹配和操作文本的强大工具。)不妨尝试javaScript模板字符串${}。它用于进行字符串模式匹配和组合。

让它只能是4的倍数,不妨假设:

1. 当数字只有一位,很明显只有0,4,8满足,类型定义如下:

type MuiltpleOf4L1<T extends number> = `${T}` extends `0` | '4' | '8' ? T : never;

2. 当数字只有两位,需要考虑个位和十位,类型定义如下:

type MuiltpleOf4L2<T extends number> = `${T}` extends `${2 | 4 | 6 | 8}${0 | 4 | 8}` | `${1 | 3 | 5 | 7 | 9}${2 | 6}` ? T : never;

3. 当数字是三位,类型定义如下:

type MuiltpleOf4L3<T extends number> = `${T}` extends `${number}${0 | 2 | 4 | 6 | 8}${0 | 4 | 8}` | `${number}${1 | 3 | 5 | 7 | 9}${2 | 6}` ? T : never;

到这里不禁有人提出质疑,这么列举下去,四位数、五位数、六位数...仍然是无穷尽,还是解决不了呢。

实则不然,三位数的情况已经涵盖了三位及以上所有位数。

整合之后能得到了一个专门用于判断是否是4的倍数的类型吗?

type MuiltpleOf4L1<T extends number> = `${T}` extends `0` | '4' | '8' ? T : never;
type MuiltpleOf4L2<T extends number> = `${T}` extends `${2 | 4 | 6 | 8}${0 | 4 | 8}` | `${1 | 3 | 5 | 7 | 9}${2 | 6}` ? T : never;
type MuiltpleOf4L3<T extends number> = `${T}` extends `${number}${0 | 2 | 4 | 6 | 8}${0 | 4 | 8}` | `${number}${1 | 3 | 5 | 7 | 9}${2 | 6}` ? T : never;

type MuiltpleOf4<T extends number> = MuiltpleOf4L1<T> | MuiltpleOf4L2<T> | MuiltpleOf4L3<T>;

看似十分合理,尝试运行...

const foo = <Num extends number>(num: MuiltpleOf4<Num>) => num

foo(4); // OK
foo(20); // OK
foo(120); // OK
foo(1000);// OK

很明显是适用的。大获成功!

方法3(扩展)

先实现判断一个数是否是另一个数的整数倍,再实现判断一个数是否是4的整数倍。

如何判断一个数T是否是另一个数V的整数倍?

  1. 计算 T 除以 V 的结果(整除),得到一个整除结果
  2. 计算整除结果V 的乘积,得到一个乘积结果
  3. 计算 T乘积结果之间的差值。
  4. 判断差值是否为 0。如果差值为 0,则表示 TV 的倍数。

思路有了,接下来尝试定义一些类型,使之能够判断一个数 T 是否能被4整除。步骤大致如下:

1、 定义名为 MakeArr 的类型

用于生成一个特定长度(ArrLen)的数组,数组的每个元素值为 FillNumberMakeArr 接受三个类型参数:

  • FillNumber:一个扩展自 number 的类型,表示要填充到数组的值。
  • ArrLen:表示目标数组的长度。
  • ExistArr:一个扩展自 number[] 的类型,表示当前已经生成的数组。
type MakeArr<FillNumber extends number, ArrLen, ExistArr extends number[]> = ExistArr['length'] extends ArrLen ? ExistArr : MakeArr<FillNumber, ArrLen, [FillNumber, ...ExistArr]>

2、定义名为 Arr 的类型

用于生成一个特定长度(ArrLen)的数组,Arr 接受一个类型参数:表示目标数组的长度。

type Arr<ArrLen extends number> = number extends ArrLen ? 1[] : MakeArr<1, ArrLen, []>

3、定义名为 Minus 的类型

用于计算两个数字类型 ab 的差值

type Minus<a extends number, b extends number> = Arr<a> extends [...LeftPart: Arr<b>, ...LeftPart: infer ValuePart] ? ValuePart['length'] : never

4、定义名为 PartMultiple 的类型

用于计算两个数字类型 ab 的乘积。PartMultiple 是一个递归类型,接受三个类型参数:

  • a:一个扩展自 number 的类型,表示乘数。
  • b:一个扩展自 number 的类型,表示被乘数。
  • pre:一个扩展自 number[] 的类型,表示之前累计的结果数组。
type PartMultiple<a extends number, b extends number, pre extends number[]> = b extends 0 ? pre['length'] : PartMultiple<a, Minus<b, 1>, [...Arr<a>, ...pre]>

5、定义名为 Multiple 的类型

用于是计算两个数字类型 ab 的乘积。

type Multiple<a extends number, b extends number> = PartMultiple<a, b, [

6、定义名为 PartDivide 的类型

用于计算两个数字类型 ab 的整除结果。PartDivide 是一个递归类型,接受三个类型参数:

  • a:一个扩展自 number 的类型,表示被除数。
  • b:一个扩展自 number 的类型,表示除数。
  • pre:一个扩展自 Array<number[]> 的类型,表示之前累计的结果数组。
type PartDivide<a extends number, b extends number, pre extends Array<number[]>> = a extends 0 ? pre['length'] : PartDivide<Minus<a, b>, b, [Arr<b>, ...pre]>

7、定义名为 Divide 的类型

用于是计算两个数字类型 ab 的整除结果。

type Divide<a extends number, b extends number> = PartDivide<a, b, []>

8、定义名为 IsMultipleOfSomeNum的类型

用于判断一个数字类型 T 是否是另一个数字类型 V 的倍数。

type IsMultipleOfSomeNum<T extends number,V extends number> =Minus<T,Multiple<Divide<T, V>,V>> extends 0? true:false;

9、定义名为 IsMultipleOfFour 的类型

用于判断一个数字类型 T 是否是数字4的倍数。

type IsMultipleOfFour<T extends number> = IsMultipleOfSomeNum<T,4>

整合之后

type MakeArr<FillNumber extends number, ArrLen, ExistArr extends number[]> = ExistArr['length'] extends ArrLen ? ExistArr : MakeArr<FillNumber, ArrLen, [FillNumber, ...ExistArr]>

type Arr<ArrLen extends number> = number extends ArrLen ? 1[] : MakeArr<1, ArrLen, []>

type Minus<a extends number, b extends number> = Arr<a> extends [...LeftPart: Arr<b>, ...LeftPart: infer ValuePart] ? ValuePart['length'] : never

type PartMultiple<a extends number, b extends number, pre extends number[]> = b extends 0 ? pre['length'] : PartMultiple<a, Minus<b, 1>, [...Arr<a>, ...pre]>
type Multiple<a extends number, b extends number> = PartMultiple<a, b, []>

type PartDivide<a extends number, b extends number, pre extends Array<number[]>> = a extends 0 ? pre['length'] : PartDivide<Minus<a, b>, b, [Arr<b>, ...pre]>
type Divide<a extends number, b extends number> = PartDivide<a, b, []>

/** 判断T是否是V的整数倍 */
type IsMultipleOfSomeNum<T extends number,V extends number> =Minus<T,Multiple<Divide<T, V>,V>> extends 0? true:false;

const x : IsMultipleOfSomeNum<25,4> = false;
const y : IsMultipleOfSomeNum<24,4> = true;
const z : IsMultipleOfSomeNum<25,5> = true;

/** 判断T是否是4的整数倍 */
type IsMultipleOfFour<T extends number> = IsMultipleOfSomeNum<T,4>
// 使用示例
const value1: IsMultipleOfFour<8> = true; // 正确
const value2: IsMultipleOfFour<7> = false; // 正确
const value3: IsMultipleOfFour<16> = true; // 正确
const value4: IsMultipleOfFour<17> = false; // 正确

虽然定义了很多类型,实现起来十分麻烦,写起来十分复杂,但是结果是可行的。成功!

灵感来源于以下文章 #使用 TS 类型实现自然数加减乘除

方法4

一种最简单的方法直接列举一些常用的数据,比如前100,如下定义。

type IsMultipleOfFour = 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 96 | 100

总结

在 TypeScript 中,实现这样的要求相对来说比较困难,因为它涉及到算术运算,而 TypeScript 的类型系统主要用于静态类型检查,而不是执行运算。TypeScript一般是是用来约束变量的形状的,可以约束它是number,但直接约束是4的倍数是非常难的。

若小伙伴们有什么更好的方法,欢迎在评论区留言🤞🤞🤞