Skip to content

TypeScript 类型体操

类型体操是什么

TypeScript 给 JavaScript 增加了一套静态类型系统,通过 TS Compiler 编译为 JS,编译的过程做类型检查

TS 的静态类型有些死板,因此新增了动态类型,也就是动态编程(这也叫做类型体操)

对传入的类型参数(泛型)做各种逻辑运算,产生新的类型,这就是类型编程 类型编程也就是类型体操

TypeScript 类型系统中的类型运算

条件:extends ? :

TypeScript 里的条件判断是 extends ? :,叫做条件类型(Conditional Type)比如:

ts
type res = 1 extends 2 ? true : false;

image-20221018153036405

推导:infer

如何提取类型的一部分呢?答案是 infer。

比如提取元组类型的第一个元素:

ts
type First<Tuple extends unknown[]> = Tuple extends [infer T, ...infer R]
  ? T
  : never;

type FirstRes = First<[1, 2, 3]>;

image-20221018153407009

联合:|

联合类型(Union)类似 js 里的或运算符 |,但是作用于类型,代表类型可以是几个类型之一。

ts
type Union = 1 | 2 | 3;

交叉:&

交叉类型(Intersection)类似 js 中的与运算符 &,但是作用于类型,代表对类型做合并。

ts
type ObjType = { a: number } & { c: boolean };

image-20221018153805453

注意,同一类型可以合并,不同的类型没法合并,会被舍弃:

image-20221018153909367

映射类型

对象、class 在 TypeScript 对应的类型是索引类型(Index Type),那么如何对索引类型作修改呢?

答案是映射类型

ts
type MapType<T> = {
  [Key in keyof T]?: T[Key];
};

keyof T 是查询索引类型中所有的索引,叫做索引查询

T[Key] 是取索引类型某个索引的值,叫做索引访问

in 是用于遍历联合类型的运算符。

比如我们把一个索引类型的值变成 3 个元素的数组:

ts
type MapType<T> = {
  [Key in keyof T]: [T[Key], T[Key], T[Key]];
};

type res = MapType<{ a: 1; b: 2 }>;

image-20221018154225087

类型体操顺口溜

模式匹配做提取,重新构造做变换。

递归复用做循环,数组长度做计数。

联合分散可简化,特殊特性要记清。

基础扎实套路熟,类型体操可通关。

模式匹配做提取

就像字符串可以通过正则提取子串一样,TypeScript 的类型也可以通过匹配一个模式类型来提取部分类型到 infer 声明的局部变量中返回。

比如提取函数类型的返回值类型:

ts
type GetReturnType<Func extends Function> = Func extends (
  ...args: any[]
) => infer ReturnType
  ? ReturnType
  : never;

image-20221014114904329

例子

ts
// 套路一: 模式匹配做提取
// 提取 value 的类型
type GetValue<P> = P extends Promise<infer value> ? value : never;
type GetValueRes = GetValue<Promise<"vvv">>;

// 删除数组第一个
type ShiftArr<T extends unknown[]> = T extends []
  ? []
  : T extends [infer Fistr, ...infer Rest]
  ? Rest
  : never;
type ShiftArrRes = ShiftArr<[1, 2, 3]>;

// 判断字符串是否以某个前缀开头
type StartsWith<
  Str extends string,
  perfix extends string
> = Str extends `${perfix}${string}` ? true : false;
type StartsWithType = StartsWith<"vvvcode", "vvv">;

// 字符串替换
type ReplaceStr<
  Str extends string,
  From extends string,
  To extends string
> = Str extends `${infer perfix}${From}${infer suffix}`
  ? `${perfix}${To}${suffix}`
  : Str;

// ?替换嘻嘻嘻嘻
type ReplaceStrRes = ReplaceStr<"vvv is boy hha ?", "?", "嘻嘻嘻嘻">;

// 去掉字符串的空白符(因为空白符很多, 需要递归)
type Trim<Str extends string> = Str extends `${infer Rest}${" "}`
  ? Trim<Rest>
  : Str;
type TrimRes = Trim<"vvvv                 ">;

// 提取函数参数类型
type GetParametersType<Func extends Function> = Func extends (
  ...args: infer Args
) => unknown
  ? Args
  : never;
type GetParametersTypeRes = GetParametersType<(a: number, b: string) => string>;

// 提取函数返回类型
type GetFnReturnType<Func extends Function> = Func extends (
  ...args: any[]
) => infer ReturnType
  ? ReturnType
  : never;
type GetFnReturnTypeRes = GetFnReturnType<(a: number, b: string) => string>;

// 提取this类型
class Vvv {
  name: string;

  constructor() {
    this.name = "dong";
  }

  hello(this: Vvv) {
    return "hello, I'm " + this.name;
  }
}
const vvv = new Vvv();
vvv.hello();

// vvv.hello.call({xxx:1});
type GetThisType<T> = T extends (this: infer ThisType, ...args: any[]) => any
  ? ThisType
  : unknown;
type GetThisTypeRes = GetThisType<typeof vvv.hello>;

// 构造器
interface Person {
  name: string;
}

interface PersonConstructor {
  new (name: string): Person;
}
// 提取构造器(实例对象)
type GetConstructorInstance<Constructor extends new (...args: any) => any> =
  Constructor extends new (...args: any) => infer Instance ? Instance : any;
type GetConstructorInstanceRes = GetConstructorInstance<PersonConstructor>;

// 提取构造器参数类型
type GetConstructorParamsters<Constructor extends new (...args: any) => any> =
  Constructor extends new (...args: infer Paramsters) => any ? Paramsters : any;
type GetConstructorParamstersRes = GetConstructorParamsters<PersonConstructor>;

// 提取ref值类型
type GetRefProps<Props> = "ref" extends keyof Props
  ? Props extends { ref?: infer Value | undefined }
    ? Value
    : never
  : never;
type GetRefPropsRes = GetRefProps<{ ref: 1 }>;
type GetRefPropsRes2 = GetRefProps<{ a: 1 }>;

模式匹配做提取

重新构造做变换

TypeScript 类型系统可以通过 type 声明类型变量,通过 infer 声明局部变量,类型参数在类型编程中也相当于局部变量,但是它们都不能做修改,想要对类型做变换只能构造一个新的类型,在构造的过程中做过滤和转换。

在字符串、数组、函数、索引等类型都有很多应用,特别是索引类型。

比如把索引变为大写:

ts
type UppercaseKey<Obj extends Record<string, any>> = {
  [Key in keyof Obj as Uppercase<Key & string>]: Obj[Key];
};

image-20221014115114882

ts
// 重新构造叫变换

// 数组类型的重新构造
// Push

type Push<Arr extends unknown[], Ele> = [...Arr, Ele];
type PushRes = Push<[1, 2, 3], "123">;

// Zip(元组合并)
type Zip<
  One extends [unknown, unknown],
  Other extends [unknown, unknown]
> = One extends [infer OneFirst, infer OneSecond]
  ? Other extends [infer OtherFirst, infer OtherSecond]
    ? [[OneFirst, OtherSecond], [OneSecond, OtherFirst]]
    : []
  : [];

type tuple1 = [1, 2];
type tuple2 = ["vvv", "code"];
type ZipRes = Zip<tuple1, tuple2>;

// 如果是任意个呢?
type Zip2<One extends unknown[], Other extends unknown[]> = One extends [
  infer OneFirst,
  ...infer OneRest
]
  ? Other extends [infer OtherFirst, ...infer OtherRest]
    ? [[OneFirst, OtherFirst], ...Zip2<OneRest, OtherRest>]
    : []
  : [];

type tuple3 = [1, 2, 3, 4, 5];
type tuple4 = ["code1", "code2", "code3", "code4", "code5"];
type ZipRes2 = Zip2<tuple3, tuple4>;

// 字符串类型的重新构造
// CapitalizeStr -- 把一个字符串字面量类型的 'vvv' 转为首字母大写的 'Vvv'。
type CapitalizeStr<Str extends string> =
  Str extends `${infer First}${infer Rest}`
    ? `${Uppercase<First>}${Rest}`
    : Str;
type CapitalizeStrRes = CapitalizeStr<"vvv">;

// CamelCase -- 实现vvv_vvv_vvv 到 vvvVvvVvv 的变换
type CamelCase<Str extends string> =
  Str extends `${infer Left}_${infer Right}${infer Rest}`
    ? `${Left}${Uppercase<Right>}${CamelCase<Rest>}`
    : Str;
type CamelCaseRes = CamelCase<"vvv_vvv_vvv">;

// DropSubStr 删除指定字符串   vvv___ 删除_ => vvv

type DropSubStr<
  Str extends string,
  SubStr extends string
> = Str extends `${infer Perfix}${SubStr}${infer Suffix}`
  ? DropSubStr<`${Perfix}${Suffix}`, SubStr>
  : Str;

type DropSubStrRes = DropSubStr<"vvv___", "_">;

// 函数类型的重新构造:
// AppendArgument 添加多一个参数
type AppendArgument<Fun extends Function, Arg> = Fun extends (
  ...args: infer Args
) => infer ReturnType
  ? (...args: [...Args, Arg]) => ReturnType
  : never;

type AppendArgumentRes = AppendArgument<
  (a: number, b: string) => string,
  number
>;

// 索引类型的重新构造
type Mapping<Obj extends object> = {
  [Key in keyof Obj]: Obj[Key];
};
type MappingRes = Mapping<{ a: 1; b: 2; c: 3 }>;

// UppercaseKey
type UppercaseKey<Obj extends object> = {
  // 因为索引可能为 string、number、symbol 类型, 这里只能接受 string 类型,所以要 & string,也就是取索引中 string 的部分。
  [Key in keyof Obj as Uppercase<Key & string>]: Obj[Key];
};
type UppercaseKeyRes = UppercaseKey<{ a: 1; b: 2; c: 3 }>;

// 约束objet --- Record
type RecordRes = Record<"test", 3>;
type UppercaseKey2<Obj extends Record<string, any>> = {
  [Key in keyof Obj as Uppercase<Key & string>]: Obj[Key];
};
type Uppercase2KeyRes = UppercaseKey<{ a: 1; b: 2; c: 3 }>;

// ToReadonly 添加 readonly 的修饰符
type ToReadonly<T> = {
  readonly [Key in keyof T]: T[Key];
};
type ToReadonlyRes = ToReadonly<{ name: "vvv"; age: 18 }>;

// ToPartial 添加可选修饰符
type ToPartial<T> = {
  [Key in keyof T]?: T[Key];
};
type ToPartialRes = ToPartial<{ name: "vvv"; age: 18 }>;

// ToMutable 移除readonly修复师
type ToMutable<T> = {
  -readonly [Key in keyof T]: T[Key];
};

type ToMutableRes = ToMutable<{ readonly name: "vvv"; readonly age: 18 }>;

// ToRequired 去掉?可选修饰符
type ToRequired<T> = {
  [Key in keyof T]-?: T[Key];
};
type ToRequiredRes = ToRequired<{ name?: "vvv"; age?: 18 }>;

// FilterByValueType  在构造新索引类型的时候根据值的类型做下过滤:

type FilterByValueType<Obj extends Record<string, any>, ValueType> = {
  [Key in keyof Obj as Obj[Key] extends ValueType ? Key : never]: Obj[Key];
};

interface Person {
  name: string;
  age: number;
  hobby: string[];
}
type FilterByValueTypeRes = FilterByValueType<Person, number>;

重新构造叫变换

递归复用做循环

在 TypeScript 类型编程中,遇到数量不确定问题时,就要条件反射的想到递归,每次只处理一个类型,剩下的放到下次递归,直到满足结束条件,就处理完了所有的类型。

比如把长度不确定的字符串转为联合类型:

ts
type StringToUnion<Str extends string> =
  Str extends `${infer First}${infer Rest}`
    ? First | StringToUnion<Rest>
    : never;

image-20221014115302944

ts
// 递归复用做循环

// Promise 的递归复用
// DeepPromiseValueType  实现一个提取不确定层数的 Promise 中的 value 类型的高级类型。

type DeepPromiseValueType<P extends Promise<unknown>> = P extends Promise<
  infer ValueType
>
  ? ValueType extends Promise<unknown>
    ? DeepPromiseValueType<ValueType>
    : ValueType
  : never;

type test = Promise<Promise<Promise<string>>>;
type DeepPromiseValueTypeRes = DeepPromiseValueType<test>;

// 简化
type DeepPromiseValueType2<T> = T extends Promise<infer ValueType>
  ? DeepPromiseValueType2<T>
  : T;
type test2 = Promise<number>;
type DeepPromiseValueType2Res = DeepPromiseValueType<test2>;

// 数组类型的递归
// ReverseArr 把type arr = [1,2,3,4,5]; 变成 type arr = [5,4,3,2,1];

type ReverseArr<Arr extends unknown[]> = Arr extends [
  infer First,
  ...infer Rest
]
  ? [...ReverseArr<Rest>, First]
  : Arr;
type ReverseArrRes = ReverseArr<[1, 2, 3, 4, 5]>;

// Includes 比如查找 [1, 2, 3, 4, 5] 中是否存在 4,是就返回 true,否则返回 false。
type IsEqual<A, B> = (A extends B ? true : false) &
  (B extends A ? true : false);
type Includes<Arr extends unknown[], FindItem> = Arr extends [
  infer First,
  ...infer Rest
]
  ? IsEqual<First, FindItem> extends true
    ? true
    : Includes<Rest, FindItem>
  : false;

type IncludesRe = Includes<[1, 2, 3, 4], 4>;

// RemoveItem 可以查找自然就可以删除,只需要改下返回结果,构造一个新的数组返回。
type RemoveItem<
  Arr extends unknown[],
  Item,
  Result extends unknown[] = []
> = Arr extends [infer First, ...infer Rest]
  ? IsEqual<First, Item> extends true
    ? // 如果存在,就不将first放入result
      RemoveItem<Rest, Item, Result>
    : // 如果不存在, 就放入
      RemoveItem<Rest, Item, [...Result, First]>
  : Result;

type RemoveItemRes = RemoveItem<[1, 2, 3, 4, 2, 2], 2>;

// 字符串类型递归
// ReplaceAll
type ReplaceAll<
  Str extends string,
  From extends string,
  To extends string
> = Str extends `${infer Left}${From}${infer Right}`
  ? `${Left}${To}${ReplaceAll<Right, From, To>}`
  : Str;

type ReplaceAllRes = ReplaceAll<"vvv_vvv_vvv", "vvv", "123">;

// StringToUnion
type StringToUnion<Str extends string> =
  Str extends `${infer First}${infer Rest}`
    ? First | StringToUnion<Rest>
    : never;
type StringToUnionRes = StringToUnion<"hello">;

// ReverseStr
type ReverseStr<Str extends string> = Str extends `${infer First}${infer Rest}`
  ? `${ReverseStr<Rest>}${First}`
  : Str;
type ReverseStrRes = ReverseStr<"hello">;

// 对象类型的递归 -- 数量(层数)不确定,类型体操中应该自然的想到递归。

type DeepReadonly<Obj extends Record<string, any>> = Obj extends any
  ? {
      readonly [Key in keyof Obj]: Obj[Key] extends object
        ? Obj[Key] extends Function
          ? Obj[Key]
          : DeepReadonly<Obj[Key]>
        : Obj[Key];
    }
  : never;

type obj = {
  a: {
    b: {
      c: {
        f: () => "dong";
        d: {
          e: {
            vvv: string;
          };
        };
      };
    };
  };
};

type DeepReadonlyResult = DeepReadonly<obj>;

递归复用做循环

数组长度做计数

TypeScript 类型系统没有加减乘除运算符,但是可以构造不同的数组再取 length 来得到相应的结果。这样就把数值运算转为了数组类型的构造和提取。

比如实现减法:

ts
type BuildArray<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr["length"] extends Length ? Arr : BuildArray<Length, Ele, [...Arr, Ele]>;

type Subtract<
  Num1 extends number,
  Num2 extends number
> = BuildArray<Num1> extends [...arr1: BuildArray<Num2>, ...arr2: infer Rest]
  ? Rest["length"]
  : never;

image-20221014115410632

联合分散可简化

TypeScript 对联合类型做了特殊处理,当遇到字符串类型或者作为类型参数出现在条件类型左边的时候,会分散成单个的类型传入做计算,最后把计算结果合并为联合类型。

ts
type UppercaseA<Item extends string> = Item extends "a"
  ? Uppercase<Item>
  : Item;

image-20221014115652971

这样虽然简化了类型编程,但也带来了一些认知负担。

比如联合类型的判断是这样的:

ts
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;

联合类型做为类型参数直接出现在条件类型左边的时候就会触发 distributive 特性,而不是直接出现在左边的时候不会。

所以, A 是单个类型、B 是整个联合类型。通过比较 A 和 B 来判断联合类型。

特殊特性要记清

会了提取、构造、递归、数组长度计数、联合类型分散这 5 个套路以后,各种类型体操都能写,但是有一些特殊类型的判断需要根据它的特性来,所以要重点记一下这些特性。

比如 any 和任何类型的交叉都为 any,可以用来判断 any 类型:`

ts
type IsAny<T> = "vvv" extends "code" & T ? true : false;

image-20221014115851446

比如索引一般是 string,而可索引签名不是,可以根据这个来过滤掉可索引签名:

ts
type RemoveIndexSignature<Obj extends Record<string, any>> = {
  [Key in keyof Obj as Key extends `${infer Str}` ? Str : never]: Obj[Key];
};

image-20221014120056764

基础扎实套路熟,类型体操可通关

基础指的是 TypeScript 类型系统中的各种类型,以及可以对它们做的各种类型运算逻辑,这是类型编程的原材料。

但是只是会了基础不懂一些套路也很难做好类型编程,所以要熟悉上面 6 种套路。

基础扎实、套路也熟了之后,各种类型编程问题都可以搞定,也就是“通关”。

参考链接

[TypeScript 类型体操通关秘籍]



⭐️⭐️⭐️ 好啦!!!本文章到这里就结束啦。⭐️⭐️⭐️

✿✿ ヽ(°▽°)ノ ✿

撒花 🌸🌸🌸🌸🌸🌸

上次更新于: