ts中数组、字符串和对象类型的递归

264 阅读4分钟

TypeScript 类型系统不支持循环,但支持递归。当处理数量(个数、长度、层数)不固定的类型的时候,可以只处理一个类型,然后递归的调用自身处理下一个类型,直到结束条件也就是所有的类型都处理完了,就完成了不确定数量的类型编程,达到循环的效果。

在类型体操里,遇到数量不确定的问题,就要条件反射的想到递归。 比如数组长度不确定、字符串长度不确定、索引类型层数不确定等。

1、Promise 的递归

1、提取不确定层数的 Promise 中的 value 类型

type ttt30 = Promise<Promise<Promise<Record<string, any>>>>;

type DeepPromiseValueType30<P extends Promise<unknown>> =
  P extends Promise<infer ValueType>
  ? ValueType extends Promise<unknown>
  ? DeepPromiseValueType30<ValueType>
  : ValueType
  : never;
//P 是待处理的 Promise,通过 extends 约束为 Promise 类型
//value 类型不确定,设为 unknown
//通过模式匹配提取出 value 的类型到 infer 声明的局部变量 ValueType 中
//然后判断如果 ValueType 依然是 Promise类型,就递归处理。
//结束条件就是 ValueType 不为 Promise 类型,那就处理完了所有的层数

type ttt30_a = DeepPromiseValueType30<ttt30>;
//type ttt30_a = {
//  [x: string]: any;
//}

//可以进一步的简化
type DeepPromiseValueType30_2<T> =
  T extends Promise<infer ValueType>
  ? DeepPromiseValueType30_2<ValueType>
  : T;

2、数组类型的递归

1、反转元组类型

type arr31 = [1, 2, 3, 4, 5];
type ReverseArr31<Arr extends unknown[]> =
  Arr extends [infer First, ...infer Rest]
  ? [...ReverseArr31<Rest>, First]
  : Arr;
type arr31_a =ReverseArr31<arr31>;
//type arr31_a = [5, 4, 3, 2, 1]

2、查找元素

type Includes31<Arr extends unknown[], FindItem> =
  Arr extends [infer First, ...infer Rest]
  ? IsEqual31<First, FindItem> extends true
  ? true
  : Includes31<Rest, FindItem>
  : false;

type IsEqual31<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
//相等的判断就是 A 是 B 的子类型并且 B 也是 A 的子类型

type testIncludes31 = Includes31<['a', 'b', 'c'], 'a'>;
//type testIncludes31 = true

3、删除元素

//RemoveItem:删除元素
type RemoveItem31<
  Arr extends unknown[],
  Item,
  Result extends unknown[] = []
> = Arr extends [infer First, ...infer Rest]
  ? IsEqual31<First, Item> extends true
  ? RemoveItem31<Rest, Item, Result>
  : RemoveItem31<Rest, Item, [...Result, First]>
  : Result;
//通过模式匹配提取数组中的一个元素的类型
//如果是 Item 类型的话就删除,也就是不放入构造的新数组,直接返回之前的 Result
//否则放入构造的新数组,也就是再构造一个新的数组 [...Result, First]
//直到模式匹配不再满足,也就是处理完了所有的元素,返回这时候的 Result

type testRemoveItem31 = RemoveItem31<['a', 'b', 'c'], 'a'>;
//type testRemoveItem31 = ["b", "c"]

4、数组类型的构造

我们学过数组类型的构造,如果构造的数组类型元素个数不确定,也需要递归。

type BuildArray31<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr['length'] extends Length
  ? Arr
  : BuildArray31<Length, Ele, [...Arr, Ele]>;
//类型参数 Length 为数组长度,约束为 number。类型参数 Ele 为元素类型
//默认值为 unknown。类型参数 Arr 为构造出的数组,默认值是 []。
//每次判断下 Arr 的长度是否到了 Length
//是的话就返回 Arr,否则在 Arr 上加一个元素,然后递归构造

//比如传入 5 和元素类型,构造一个长度为 5 的该元素类型构成的数组。
type testBuildArray31 = BuildArray31<5, number>
//type testBuildArray31 = [number, number, number, number, number]

3、字符串类型的递归

1、把一个字符串中的某个字符替换成另一个

type ReplaceAll32<
  Str extends string,
  From extends string,
  To extends string
> = Str extends `${infer Left}${From}${infer Right}`
  ? `${Left}${To}${ReplaceAll32<Right, From, To>}`
  : Str;
//类型参数 Str 是待处理的字符串类型,From 是待替换的字符,To 是替换到的字符。
//通过模式匹配提取 From 左右的字符串到 infer 声明的局部变量 Left 和 Right 里。
//用 Left 和 To 构造新的字符串,剩余的 Right 部分继续递归的替换。
//结束条件是不再满足模式匹配,也就是没有要替换的元素,这时就直接返回字符串 Str。

type testReplaceAll32=ReplaceAll32<'Hello my name is Hello','Hello','jack'>
//type testReplaceAll32 = "jack my name is jack"

2、把字符串字面量类型的每个字符都提取出来组成联合类型

//'dong' 转为 'd' | 'o' | 'n' | 'g'。
type StringToUnion32<Str extends string> =
  Str extends `${infer First}${infer Rest}`
  ? First | StringToUnion32<Rest>
  : never;
//类型参数 Str 为待处理的字符串类型,通过 extends 约束为 string。
//通过模式匹配提取第一个字符到 infer 声明的局部变量 First,其余的字符放到局部变量 Rest。
//用 First 构造联合类型,剩余的元素递归的取。

type testStringToUnion32=StringToUnion32<'hello'>;
//type testStringToUnion32 = "e" | "h" | "o" | "l"

3、字符串类型的反转

type ReverseStr32<
  Str extends string,
  Result extends string = ''
> = Str extends `${infer First}${infer Rest}`
  ? ReverseStr32<Rest, `${First}${Result}`>
  : Result;

type testReverseStr32 = ReverseStr32<'hello'>
//type testReverseStr32 = "olleh"

4、对象类型的递归

1、给任意层数的索引类型的添加 readonly 修饰

type DeepReadonly33<Obj extends Record<string, any>> = {
  readonly [Key in keyof Obj]:
  Obj[Key] extends object
  ? Obj[Key] extends Function
  ? Obj[Key]
  : DeepReadonly33<Obj[Key]>
  : Obj[Key]
}
//值要做下判断,如果是 object 类型并且还是 Function,那么就直接取之前的值 Obj[Key]
//如果是 object 类型但不是 Function,那就是说也是一个索引类型,就递归处理 DeepReadonly<Obj[Key]>
//数量(层数)不确定,类型体操中应该自然的想到递归
type obj33 = {
  a: {
    b: {
      c: {
        f: () => 'hello',
        d: {
          e: {
            score: string
          }
        }
      }
    }
  }
}

type testobj33 = DeepReadonly33<obj33>['a'];
//为啥这里没有计算呀?
//因为 ts 的类型只有被用到的时候才会做计算。
//type testobj33 = {
//  readonly b: DeepReadonly33<{
//    c: {
//        f: () => 'hello';
//        d: {
//            e: {
//                score: string;
//            };
//        };
//    };
//}>;
//}

//所以可以在前面加上一段 Obj extends never ? never 或者 Obj extends any 等,从而触发计算:
type DeepReadonly33_1<Obj extends Record<string, any>> =
  Obj extends any
  ? {
    readonly [Key in keyof Obj]:
    Obj[Key] extends object
    ? Obj[Key] extends Function
    ? Obj[Key]
    : DeepReadonly33_1<Obj[Key]>
    : Obj[Key]
  }
  : never;
type testobj33_1 = DeepReadonly33_1<obj33>;
/*
type testobj33_1 = {
    readonly a: {
        readonly b: {
            readonly c: {
                readonly f: () => 'hello';
                readonly d: {
                    readonly e: {
                        readonly score: string;
                    };
                };
            };
        };
    };
}
*/