1

Typescript类型编程奇奇怪怪的写法对于刚接触的朋友比较陌生,这份资料目的就是按编程脉络,将Typescript类型编程的写法罗列出来,方便编写时查询。

在理解完基础逻辑后,推荐配合type-challenges的部分题目来熟悉语法,TypeHero 也是一个很不错的选择。由于只谈类型编程,阅读时需要对Typescript有一定的了解。

所有的示例可以黏贴到这里做测试:https://www.typescriptlang.org/zh/play

类型

既然是类型编程,那么类型就是最基础的元素,先来回忆一下。

类型定义

原始类型:

  • boolean
  • string
  • number
  • bigint
  • symbol

复合类型:

  • objectObject:对象类型。

    • Object类型代表广义的对象,即JavaScript中所有可以被视为对象的值。可以使用{}替代表示。
    • object类型代表比较精确的对象,即对象、数组、函数。
  • Array[]:数组或者元组

    • 数组,所有成员类型都相同,且个数不固定
    • 元组,所有成员类型可以不相同,且个数固定
  • enum:枚举类型

特殊类型:

  • void:没返回值
  • undefined:受strictNullChecks参数控制,可以开启赋值给任意类型
  • null:受strictNullChecks参数控制,可以开启赋值给任意类型
  • never:空类型,无法接收任意类型,可以赋值给任意类型
  • unknown:可以接收任意类型,无法赋值给任意类型
  • any:可以赋值给任意类型,也能接收任意类型,即关闭类型检查

值类型:

  • 变量可以配置具体的某个值当做类型

其他构造函数:

  • Function
  • Error
  • Date
  • ...等等

联合与交叉类型

  • |:联合类型,可以将原始类型、复合类型、特殊类型或值类型组合成一个联合类型
  • &:交叉类型,可以将原始类型、复合类型组合成一个交叉类型

类型属性符

  • ?:可选属性
  • readonly:只读属性。例如:type n = readonly numbertype n = Readonly<number>
  • as const:效果与只读属性一样,可以在末尾直接添加 as const用来快速配置只读属性。例如const arr = [1, 2, 3] as const;
  • unique:Symbol 类型使用unique symbol来表达具体的Symbol实例。

类型运算符

除了类型,Typescript还提供了一些必要的逻辑运算关键字:

  • typeof:返回运算目标的类型
  • keyof:返回运算目标所有的key
  • in:检查指定类型是否在目标类型中
  • []:取出对象的指定键名的类型值
  • extends ? true : false:条件运算,类似于三元运算符
  • infer:定义新的类型参数,需要在extends运算符一起使用
  • is:约束类型
  • ``:字符串模板,其中可以使用${}来引用 string、number、bigint、boolean、null、undefined这六种类型,也可以做字符串组合与匹配。
  • ...:扩展运算符。用来处理不确定的剩余数据,标注类型只能是数组或元组
  • +:可以给类型添加某个属性
  • -:可以给类型去除某个属性
  • as<>:类型断言。可以将一种类型断言成其他类型
  • satisfies:检查某个值是否符合指定类型

对于用过Typescript的朋友可能会有点印象,但是到类型编程里需要怎样书写会比较懵,在接下来的案例中我们将用到这些运算符。

类型编程逻辑元素

既然是类型编程,我们就按平常编程归纳一下步骤:一般情况下会先创建变量,然后对变量进行一定的逻辑运算,最后返回值。逻辑运算中可能会需要做一些基础类型判断、分支语句逻辑与遍历逻辑。

变量是什么?变量就是我们需要处理的对象。Typescript没有提供let const之类类型关键字,如何定义变量呢?Typescript没办法回车换行,在处理过程中又需要怎么创建新的处理对象?类型参数一节会罗列这块写法。

逻辑处理就是顺序执行、条件分支以及数据循环,顺序执行不用多说,在谈论完类型参数后会罗列条件分支、数据循环的写法。

这两块熟悉完后对于类型编程已经能上手了,之后就是列举一些常见的处理场景,来熟练巩固语法的写法。

内置类型工具

Typescript有内置一部分类型工具,可以简化部分处理逻辑:链接

类型参数

类型编程里因为创建的符号实际上不能重新赋值,所以我们称之为类型参数。创建有两种形式:

  1. 直接在尖括号里定义参数

    type GetType<T> = T;
    
    type ret = GetType<string>; // type ret = string

    赋予默认值

    type GetTypeNumber<T = number> = T;
    
    type ret = GetTypeNumber; // type ret = number
    

    约束类型

    type GetArgStringOrNumber<T extends string | number> = T;
    
    type ret = GetArgStringOrNumber<bigint>; // Type 'bigint' does not satisfy the constraint 'string | number'.(2344)
    
  2. 使用infer关键字创建

    infer关键字大部分情况下需要接在extends关键字之后。

    比如,可以获取Promise中值的类型

    type GetType<T> = T extends Promise<infer V> ? V : T;
    
    type ret = GetType<Promise<{}>>; // type ret = {}
    

    可以利用字符串模板的匹配能力提取部分字符串,比如提取首字母

    type GetFirst<T> = T extends `${infer first}${string}` ? first : T;
    
    type ret = GetFirst<'First'>; // type ret = "F"
    

    可以提取数组指定位的信息

    type GetLast<T> = T extends [...infer _, infer last] ? last : T;
    
    type ret = GetLast<[1, 2, 3]>; // type ret = 3
    

    在函数定义上可以单独使用infer关键字

    比如剩余参数的类型集合

    type GetParameters<F extends Function> = F extends (
     ...args: infer Args
    ) => unknown
     ? Args
     : never;
    
    type ret = GetParameters<(a1: string, a2: string) => string>; // type ret = [a1: string, a2: string]

    或者返回值的推断

    type MyReturnType<T> = T extends (...argv:any[]) => infer T ? T :never;
    
    type ret = MyReturnType<()=> Promise<{ code: 1 }>>; // type ret = Promise<{ code: 1; }>

类型收缩与转换

  • as

    as可以直接将类型断言为另外无关的类型,很常见就不多举例了。

  • extends

    extends可以对类型进行约束收缩,避免出现类型报错。以上已经有应用示例。

  • &

    交叉类型运算可以让类型参数中符合条件的保留下来(做交集)。

    type ret1 = ('str1' | 'str2' | 123) & string; // type ret1 = "str1" | "str2"
    
    type ret2 = ('str1' | 'str2' | 123) & (string | number); // type ret1 = "str1" | "str2" | 123
    

条件分支

有了参数之后我们往往需要对数据做判断,这就涉及到条件分支语句。

在Typescript中没有if else语句,但有个类似三元运算符的 extends ? :语法(上面infer语法举例已经用到了),我们用此来替代条件分支。

extends用到的场景有点多,单独使用时是继承,继承限制了范围,所以也可以拿来做类型约束。在之后接上? : 就变成了条件分支。在这之间可以用infer关键字来创建新的类型参数。

比如我们要判断一个类型是否为string,首先使用extends约束类型,后面接上? :,当为true、false时返回我们所需要的内容。

type IsString<T> = T extends string ? true : false;

type ret = IsString<'hello'>; // type ret = true

判断数据为数组

type IsArray<T> = T extends unknown[] ? true : false;

type ret = IsArray<[]>; // type ret = true

多层if判断,判断类型是否为string或是number

type IsStringOrNumber<T> = T extends string ? true 
                            : T extends number ? true  
                            : false;

type ret1 = IsStringOrNumber<'hello'>; // type ret1 = true                            
type ret2 = IsStringOrNumber<987>; // type ret2 = true

循环

循环有多种场景,我们分开讨论。

  1. 使用关键字keyof

    • 取对象的所有Key

      keyof最基础的用法,遍历出目标对象的所有Key

      type GetKey<T> = keyof T;
      
      type ret = GetKey<{ name: string; age: number; }>; // type ret = keyof Person
      
    • 获取对象的所有value类型

      type GetValue<T> = T[keyof T];
      
      type ret = GetValue<{ name: string; age: number; }>; // type ret = string | number
      
    • 根据传入的对象动态推导出返回值类型

      配合extends 约束K的类型是T的所有Key。

      // 根据给定对象约束传入的key 推导出对应的返回值类型
      type Prop<T extends object, K extends keyof T> = T[K];
    • 对象的key增加或删除属性

      可以增加或者删减属性字段配置。

      // 增加只读属性  TS内置了Readonly工具
      type MyReadonly<T> = {
        readonly [K in keyof T]: T[K]
      }
      
      type ret = MyReadonly<{ name: string; age: number; }>; // type ret = { readonly name: string; readonly age: number; }
      
      // 删除可选属性  TS内置了Required工具
      type MyRequired<T> = {
        [P in keyof T]-?: T[P]
      }
      
      type ret2 = MyRequired<{ name?: string; age?: number; }>; // type ret2 = { readonly name: string; readonly age: number; }
    • 对象key进行二次处理

      对key处理使用as重映射语法。

      这是将Key转换为首字母大写的示例,Capitalize是内置的首字母大写类型工具。

      type CapitalizeKey<T extends object> = {
        [K in keyof T as 
          K extends string ? Capitalize<K> : K
        ]: T[K];
      };
      
      type ret = CapitalizeKey<{ one: 1; two: 2; 3: 3 }>; // type ret = { One: 1; Two: 2; 3: 3; }
  2. 递归

    其他需要重复处理的形式可以用递归解决。

    递归就是在处理逻辑中调用自身,比如下面这个颠倒数组的例子,每次取出第一位的数据,然后放到重组后的数据的最后位。

    type Reversal<T> = T extends [infer first, ...infer rset] ? [...Reversal<rset>, first] : T;
    
    type ret = Reversal<[1,2,3,4,5,6]>; // type ret = [6, 5, 4, 3, 2, 1]

    另一个常用领域是处理字符串的时候,Typescript字符串模板匹配字符串的时候匹配到一个即停止,如果有多个符合条件的匹配就需要使用递归处理。(字符串模板配infer可以理解为单次的split,用已知字符去匹配目标字符串,匹配到了就分割,但只分割一次,重复的不再次分割)

    举个例子,我们需要实现去除所有下划线,将剩余字符串调用自身再次处理,直到所有匹配的字符串都处理完为止。

    type RemoveAllUnderline<T extends string> = T extends `_${infer str}` 
     ? RemoveAllUnderline<str> 
     : T;
    
    type ret = RemoveAllUnderline<'___Hello World'>; // type ret = "Hello World"

    还有一个类似的例子就是全量替换,但代码看起来会复杂一些,逻辑是一样的。

    核心依然是根据需要匹配的字符串拆分字符,这样我们就有了以匹配字符串为核心的左中右三组。中间字符串是需要替换的目标,只需要拿左侧、右侧与替换后的字符拼接就是目标字符串了。因为一次只处理一个字符,所以需要递归调用。这里因处理字符串是从左到右处理,所以左侧一定是处理过的了,只需要对右侧的字符串做递归即可。

    type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' 
     ? S 
     : S extends `${infer left}${From}${infer right}` 
       ? `${left}${To}${ReplaceAll<right, From, To>}` 
       : S;
    
    type ret = ReplaceAll<'~ ~ Hello World ~ ~', '~', '!'>; // type ret = "! ! Hello World ! !"
  3. 联合类型

    联合类型比较特殊,Typescript处理时自动会将每个类型单独传入,所以不需要特别处理遍历。以下是一个替换例子,没用到循环与递归就能将联合类型中的元素单独处理。

    type Union = 'sky' | 'lawn' | 'flower';
    
    type Replace<T> = T extends 'flower' ? `bloom` : T;
    
    type ret = Replace<Union> // type ret = "sky" | "lawn" | "bloom"

类型处理场景列举

学完以上知识点我们就可以列举一些常见场景的组合用法了。这里可以配合type-challengesTypeHero 题目训练加强理解。

字符串类型

Typescript中没有提供那么多工具函数,匹配、拆分、合并字符串全都依赖字符串模板工具。

字符串模板中有自动匹配机制,会根据已提供的字符串去拆分目标字符串。下面是一些示例:

  • 合并

    合并比较简单,直接使用${}语法来拼接数据

    type Merge<S extends string> = `Hello ${S}`;
    
    type ret = Merge<'World'>; // Hello World
  • 匹配与拆分

    字符串模板可以根据提供的字符或者类型,将字符串划分成多个部分。

    这里为了创建新的类型参数使用到了extends,其实可以将这种视为一种固定匹配格式 extends ${infer X} ? X : never; 在extends 之后的字符串模板中创建新的类型参数。

    比如分割字符串,根据下划线分割,后面部分是只要符合字符串类型即可匹配成功

    type Spilt<S extends string> = S extends `${infer first}_${string}` ?  first : never;
    
    type ret = Spilt<'Hello_World'>; // Hello

    如何后面改成number类型,传入的只能是数字(也不能有小数点),否则匹配不到。

    type Spilt<S extends string> = S extends `${infer first}_${number}` ?  first : never;
    
    type ret = Spilt<'Hello_123'>; // Hello

    字符串匹配只会匹配遇到的第一个字符,后续如果有相同字符也不会再匹配。

    type RemoveUnderline<S extends string> = S extends `_${infer str}` ? str : S;
    
    type ret = RemoveUnderline<'___Hello World'>; // type ret = "__Hello World"
    

    要连续处理的话得需要用递归的逻辑。

    type RemoveAllUnderline<S extends string> = S extends `_${infer str}` 
        ? RemoveAllUnderline<str> 
        : S;
    
    type ret = RemoveAllUnderline<'___Hello World'>; // type ret = "Hello World"

    如果要处理右侧的字符需要另外重新写匹配逻辑,这场景最经典的就是处理前后空字符串问题

    type Trim<S extends string> = TrimLeft<TrimRight<S>>;
    type TrimLeft<S extends string> = S extends ` ${infer str}` ? TrimLeft<str> : S;
    type TrimRight<S extends string> = S extends `${infer str} ` ? TrimRight<str> : S;
    
    type ret = Trim<'  Hello World  '> // type ret = "Hello World"
    
  • 转换为驼峰命名法

    逻辑依然是拆分字符串,稍微不同的是这里用了内置工具函数Uppercase来实现首字母大写能力。

    type CamelCase<S extends string> = 
        S extends `${infer left}_${infer right}${infer rest}`
            ? `${left}${Uppercase<right>}${CamelCase<rest>}`
            : S;
    
    type ret = CamelCase<'camel_case'>; // type ret = "camelCase"
  • 转为蛇形命名法

    如果需要短横命名法只需要更换一下横线即可

    type SnakeCase<S extends string> = S extends `${infer char}${infer rest}`
      ? rest extends Uncapitalize<rest>
        ? `${Uncapitalize<char>}${SnakeCase<rest>}`
        : `${Uncapitalize<char>}_${SnakeCase<rest>}`
      : S;
    
    type ret = SnakeCase<"SnakeCase">; // type ret = "snake_case"
  • 字符串转数组

    逻辑与上面一样,先取出一个字符,然后利用递归将剩余的字符串重复处理,不断展开并拼接成新数组。

    type StringToArray<S extends string> = S extends `${infer char}${infer rest}`
      ? [char, ...StringToArray<rest>]
      : [];
    
    type ret = StringToArray<"Hello World">; // type ret = ["H", "e", "l", "l", "o", " ", "W", "o", "r", "l", "d"]

数组、元组类型

数组匹配就是按位匹配,如果不确定是第几位就需要用...运算符处理。

  • 取数组第一位

    type GetFirst<T> = T extends [infer first, ...infer _] ? first: T;
    
    type ret = GetFirst<[1, 2, 3]>; // type ret = 1
  • 取数组最后一位

    type GetLast<T> = T extends [...infer _, infer last] ? last : T;
    
    type ret = GetLast<[1, 2, 3]>; // type ret = 3
  • 去除数组第一位

    不需要的位可以使用unknown关键字占位

    type Unshift<T> = T extends [unknown, ...infer last] ? last : T;
    
    type ret = Unshift<[1, 2, 3]>; // type ret = [2, 3]
  • 取数组指定位

    type GetIndexValue<T, I extends keyof T> = T[I];
    
    type ret = GetIndexValue<[1, 2, 3], 1>; // type ret = 2
  • 修改创建新数组

    将数组拆分后重组就是创建新数组,重组的时候省略某些值就是删除,在原来位写入新值就是替换,所有的基础逻辑都是数组的拆分。

    以下是一个取数组最后一位并用传入的新值创建新数组示例

    type CreateNewArr<T, U> = T extends [...infer _, infer last] ? [last, U] : T;
    
    type ret = CreateNewArr<[1, 2, 3], 4>; // type ret = [3, 4]

    我们也可以使用...运算符来展开数组,比如合并两个数组类型数据

    type Concat<T extends unknown[], U extends unknown[]> = [...T,...U];
    
    type ret = Concat<['1', 2], [3]>; // type ret = ["1", 2, 3]
    
  • 数组的长度信息

    需要获数组的数量时,同样是取length,只是写法没那么漂亮

    type GetLenght<T extends unknown[]> = T['length'];
    
    type ret = GetLenght<[0, 1, 2, 3, 4]>; // type ret = 5
    
  • 数组转字符串类型

    同样将数组元素一一取出,然后用字符串模板重新拼接元素。

    S变量是为了存储处理后的字符串,字符串模板只支持自动转换简单的类型,T是数组无法直接写成类似这样的语句${arrayToString<rest>},所以借助变量来进行处理。

    type ExtendsType = string | number | boolean | undefined | null | bigint
    type ArrayToString<
      T extends unknown[],
      S extends string = ""
    > = T extends [infer first extends ExtendsType, ...infer rest extends ExtendsType[]]
      ? ArrayToString<rest, `${S}${first}`>
      : S;
    
    type ret = ArrayToString<[0, 1, '2', 'str', 99n, true, null]> // type ret = "012str99truenull"
    

对象类型

对象类型大部分就是对索引的处理。

  • 根据传入类型约束key

    // 根据给定对象约束传入的key 推导出对应的返回值类型
    type Prop<T extends object, K extends keyof T> = T[K];
  • 提取指定Key

    type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
    
    type ret = MyPick<{ one: 1, two: 2 }, 'two'>; // type ret = { two: 2; }
  • 剔除指定Key

    Exclude是内置工具,可以获得排除指定值的数据(差集)。Pick是内置工具函数用于提取指定Key

    type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
    
    type ret = MyOmit<{ one: 1, two: 2 }, 'two'>; // type ret = { one: 1; }
    
  • 重映射语法实现首字母大写

    type CapitalizeKey<T extends object> = {
      [K in keyof T as 
        K extends string ? Capitalize<K> : K
      ]: T[K];
    };
    
    type ret = CapitalizeKey<{ one: 1; two: 2; 3: 3 }>; // type ret = { One: 1; Two: 2; 3: 3; }
  • 重新指定Value类型

    type ObjectVaule<T extends object> = {
      [K in keyof T]: 
        T[K] extends string ? `${T[K]}!` : [T[K]]
    }
    type ret = ObjectVaule<{ one: 1, two: 2, three: 'three' }>; // type ret = { one: [1]; two: [2]; three: "three!"; }
  • Value与Key位置翻转

    利用重映射语法,将值写在Key的位置,将Key写在值的位置即可

    type Flip<T extends Record<string, string | number | boolean>> = {
      [P in keyof T as `${T[P]}`]: P
    };
    
    type ret = Flip<{ one: 1, two: 2 }>; // type ret = { 1: "one"; 2: "two"; }

联合类型

联合类型与其他类型不一样,会单独传入每一项进行匹配,所以处理的时候不需要特别写循环逻辑。

  • 提取交集

    type MyExtract<T, U> = T extends U ? T : never;
    
    type ret = MyExtract<'one' | 'two', 'two' | 'three'>; // type ret = "two"
  • 提取差集

    type MyExclude<T, K> = T extends K ? never : T;
    
    type ret = MyExclude<'one' | 'two' | 'three', 'three'>; // type ret = "one" | "two"
    
  • 字符串转联合类型

    type StringToUnion<T extends string> = T extends `${infer first}${infer rest}`
      ? first | StringToUnion<rest>
      : never;
    
    type ret = StringToUnion<"Hello"> // type ret = "H" | "e" | "l" | "o"
  • 数组转联合类型

    type TupleToUnion<T extends unknown[]> = T[number];
    
    type ret = TupleToUnion<[1, 2, 'str']> // type ret = 1 | 2 | "str"
    
  • 阻断联合类型处理

    将泛型参数用[]括起来可以阻断联合类型处理。

    比如有一个类型判断工具函数,如果传入的是联合类型,每个值会有单独返回,这里一个值是true一个值是false,所以显示的是boolean。这种场景下我们需要将传入的类型视为一个整体,所以加上括号做类型阻断,这时候就能正常返回false了。

    never类型在这里也比较特殊,被认为是没有联合类型,所以直接返回never。这种场景下也是用阻断联合类型来处理就能返回正确的值了。

    type isString<T> = T extends string ? true : false;
    type ret = isString<'hello' | 123>; // type ret = boolean
    type ret2 = isString<never>; // type ret = never
    
    type isStringPlus<T> = [T] extends string ? true : false;
    type retPlus = isStringPlus<'hello' | 123>; // type retPlus = false
    type retPlus2 = isStringPlus<never>; // type retPlus2 = false

函数入参定义

  • 提取入参参数

    type GetParameters<F extends Function> = F extends (
      ...args: infer Args
    ) => unknown
      ? Args
      : never;
    
    type ret = GetParameters<(a1: string, a2: string) => string>; // type ret = [a1: string, a2: string]
  • 获取函数返回类型

    type MyReturnType<T> = T extends (...argv:any[]) => infer T ? T :never;
    
    type ret = MyReturnType<()=> Promise<{ code: 1 }>>; // type ret = Promise<{ code: 1; }>
    
  • 获取函数的This类型

    Typescript中函数定义的第一个参数可以定义this类型,用这个特性我们可以获取this的信息。

    type GetThisParameterType<T> = T extends (
      this: infer type,
      ...args: never
    ) => any
      ? type
      : unknown;
    
    type ret2 = GetThisType<
      (
        this: { text: string; print: () => string },
        a1: string,
        a2: string
      ) => string
    >; // type ret = { text: string; print: () => string; }

基础类型工具定义

条件判断

boolean是联合类型,定义类型 type Bool = false | true;

  • if

    type If<Cond extends Bool, Then, Else> = Cond extends true ? Then : Else;
  • not

    type Not<A extends Bool> = A extends true ? false : true;
  • and

    type And<A extends Bool, B extends Bool> = If<A, If<B, true, false>, false>;
  • or

    type Or<A extends Bool, B extends Bool> = If<A, true, If<B, true, false>>;

类型判断

  • Any

    type IsAny<T> = 0 extends (1 & T) ? true : false;
  • Never

    type IsNever<T> = [T] extends [never] ? true : false;
  • Boolean

    type IsBoolean<T> = T extends boolean ? true : false;
  • Number

    type IsNumber<T> = T extends number ? true : false;
  • String

    type IsString<T> = T extends string ? true : false;
  • Null

    type IsNull<T> = T extends null ? true : false;
  • Undefined

    type IsUndefined<T> = T extends undefined ? true : false;
  • Nil

     type IsNil<T> = Or<IsNull<T>, IsUndefined<T>>;
  • Array

    type IsArray<T> = T extends unknown[] ? true : false;
  • IsFunction

    type IsFunction<T> = T extends Function ? true : false;
  • Object

    type IsObject<T> = T extends object ? true : false;
    
    type IsRealObject<T> = And<
      T extends object ? true : false,
      And<Not<IsFunction<T>>, Not<IsArray<T>>>
    >;
    
  • IsUnion

    type IsUnion<T, U = T> = T extends U 
      ? ([U] extends [T] ? false : true) 
      : never;

数学运算

  • 构造指定个数数组

    Typescript没提供数学运算能力,数学运算的实现都需要借助数组长度来处理。

    type BuildArr<
      Length extends number,
      Item,
      Arr extends unknown[] = []
    > = Arr["length"] extends Length ? Arr : BuildArr<Length, Item, [...Arr, Item]>;
    
    type ret = BuildArr<3, "!">; // type ret = ["!", "!", "!"]
    
  • 加法

    type Add<T extends number, S extends number> = [
      ...BuildArr<T, number>,
      ...BuildArr<S, number>
    ]["length"];
    
    type ret = Add<2, 3>; // type ret = 5
  • 减法

    type Subtract<T extends number, S extends number> = BuildArr<
      T,
      number
    > extends [...arr1: BuildArr<S, number>, ...arr2: infer Rest]
      ? Rest["length"]
      : never;
    
    type ret = Subtract<3, 2>; // type ret = 1
  • 乘法

    type Mutiply<
      T extends number,
      S extends number,
      ResultArr extends unknown[] = []
    > = S extends 0
      ? ResultArr["length"]
      : Mutiply<T, Subtract<S, 1>, [...BuildArr<T, number>, ...ResultArr]>;
    
    type ret = Mutiply<3, 2>; // type ret = 6
  • 除法

    type Divide<
      T extends number,
      S extends number,
      CountArr extends unknown[] = []
    > = T extends 0
      ? CountArr["length"]
      : Divide<Subtract<T, S>, S, [unknown, ...CountArr]>;
    
    type ret = Divide<6, 2>; // type ret = 3

LnEoi
707 声望17 粉丝