TypeScript 类型体操 - 基础操作

2023-05-17 17:02:20 浏览数 (1)

# 前置知识

# 类型是什么

类型即 numberbooleanstring 等基础类型和 ObjectFunction 等复合类型,它们是编程语言提供的对不同内容的抽象:

不同类型变量占据的内存大小不同boolean 类型的变量会分配 4 个字节的内存,而 number 类型的变量则会分配 8 个字节的内存,给变量声明了不同的类型就代表了会占据不同的内存空间。

不同类型变量可做的操作不同number 类型可以做加减乘除等运算,boolean 就不可以,复合类型中不同类型的对象可用的方法不同,比如 DateRegExp,变量的类型不同代表可以对该变量做的操作就不同。

如果能保证对某种类型只做该类型允许的操作,就叫做类型安全类型检查目的是为了保证类型安全

在运行时类型检查叫做动态类型检查,在编译时类型检查叫做静态类型检查

# 类型系统

  • 简单类型系统
    • 变量、函数、类等都可以声明类型,编译器会基于声明的类型做类型检查
  • 支持泛型的类型系统
    • 声明时可以将变化的类型声明为泛型,编译器会根据传入的实际类型做类型检查
  • 支持类型编程的类型系统
    • 可以对传入的类型参数(泛型)做逻辑运算,产生新的类型

# TypeScript 类型系统中的类型

  • JavaScript 的运行时类型
    • boolean
    • number
    • bigint
    • string
    • symbol
    • null
    • undefined
    • object
    • 包装类型
      • Boolean
      • Number
      • BigInt
      • String
      • Symbol
      • Object
    • 复合类型
      • class
      • function
      • array
  • TypeScript 新增类型
    • Tuple
    • Enum
    • Interface
    • 特殊类型
      • any
      • unknown
      • never
      • void

# TypeScript 类型系统中的类型运算

条件:T extends U ? X : Y

代码语言:javascript复制
type IsString<T> = T extends string ? true : false;

推导:infer R

代码语言:javascript复制
// 获取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

// 获取函数参数类型
type ParamType<T> = T extends (...args: infer P) => any ? P : any;

联合:T | U

代码语言:javascript复制
type Union = string | number;

交叉:T & U

代码语言:javascript复制
type Person = { name: string } & { age: number };
// 等价于
type Person = { name: string; age: number };

// 注意,不同类型的交叉不会合并
type res = string & number; // never

映射类型:{ [K in keyof T]: X }

代码语言:javascript复制
type Person = { name: string; age: number };
type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };

  • keyof T:获取 T 的所有属性名组成的联合类型
  • T[K]:获取 T 的属性 K 的类型
  • in:遍历

# 模式匹配

字符串使用正则做模式匹配:

代码语言:javascript复制
const str = "hello world";
const reg = /hello (w )/;
const res = str.replace(reg, "Hi, $1"); // Hi, world

TypeScript 类型做模式匹配:

代码语言:javascript复制
type CustomP = Promise<"Cell">;

type GetValueType<T> = T extends Promise<infer R> ? R : never;

type res = GetValueType<CustomP>; // Cell

const val: res = "Cell"; // ok

Typescript 类型的模式匹配是通过 extends 对类型参数做匹配,结果保存到通过 infer 声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。

# 数组类型

代码语言:javascript复制
type arr = [number, string, boolean];

type GetFirst<Arr extends unknown[]> = Arr extends [infer First, ...unknown[]] ? First : never;

type GetLast<Arr extends unknown[]> = Arr extends [...unknown[], infer Last] ? Last : never;

type PopArr<Arr extends unknown[]> = Arr extends []
  ? []
  : Arr extends [...infer Rest, unknown]
  ? Rest
  : never;

type ShiftArr<Arr extends unknown[]> = Arr extends []
  ? []
  : Arr extends [unknown, ...infer Rest]
  ? Rest
  : never;

`any` 和 `unknown` 的区别

anyunknown 都代表任意类型,但是 any 是类型系统的顶级类型,可以赋值给任意类型,而 unknown 是类型系统的底级类型,不能赋值给任意类型,只能赋值给 any 或者 unknown

# 字符串类型

代码语言:javascript复制
type str = "hello world";

type StartWith<Str extends string, Prefix extends string> = Str extends `${Prefix}${infer Rest}`
  ? true
  : false;

type EndWith<Str extends string, Suffix extends string> = Str extends `${infer Rest}${Suffix}`
  ? true
  : false;

type ReplaceStr<
  Str extends string,
  From extends string,
  To extends string
> = Str extends `${Prefix}${From}${Suffix}` ? `${Prefix}${To}${Suffix}` : Str;

type TrimStrRight<Str extends string> = Str extends `${infer Rest}${" " | "t" | "n"}`
  ? TrimStrRight<Rest>
  : Str; // 去除字符串右边的空格

type TrimStrLeft<Str extends string> = Str extends `${" " | "t" | "n"}${infer Rest}`
  ? TrimStrLeft<Rest>
  : Str; // 去除字符串左边的空格

type TrimStr<Str extends string> = TrimStrRight<TrimStrLeft<Str>>; // 去除字符串两边的空格

# 函数

代码语言:javascript复制
type GetParameters<Func extends Function> = Func extends (...args: infer Args) => unknown
  ? Args
  : never;

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

# 构造器

代码语言:javascript复制
interface Person {
  name: string;
  age: number;
}

interface PersonConstructor {
  new (name: string, age: number): Person;
}

type GetInstanceType<ConstructorType extends new (...args: any) => any> =
  ConstructorType extends new (...args: any) => infer InstanceType ? InstanceType : any;

type GetConstructorParameters<ConstructorType extends new (...args: any) => any> =
  ConstructorType extends new (...args: infer ParametersType) => any ? ParametersType : never;

# 索引类型

代码语言:javascript复制
type GetRefPropType<Props> = "ref" extends keyof Props
  ? Props extends { ref?: infer RefType | undefined }
    ? RefType
    : never
  : never;

TypeScript 类型的模式匹配是通过类型 extends 一个模式类型,把需要提取的部分放到通过 infer 声明的局部变量里,后面可以从这个局部变量拿到类型做各种后续处理。

# 重新构造

类型编程主要的目的就是对类型做各种转换,TypeScript 类型系统支持 3 种可以声明任意类型的变量: typeinfer、类型参数。

代码语言:javascript复制
// type 类型别名,声明一个变量存储某个类型
type P = Promise<string>;

// infer 用于类型的提取,然后存到一个变量里,相当于局部变量
type GetValueType<T> = T extends Promise<infer R> ? R : never;

// 类型参数用于接受具体的类型,在类型运算中也相当于局部变量
type isNumber<T> = T extends number ? true : false;

严格来说这三种也都不叫变量,因为它们不能被重新赋值

TypeScript 的 typeinfer、类型参数声明的变量都不能修改,想对类型做各种变换产生新的类型就需要重新构造

# 数组类型

代码语言:javascript复制
type tuple = [1, 2, 3];

type PushArr<Arr extends unknown[], Item> = [...Arr, Item]; // 在数组尾部添加元素
type PushResult = PushArr<tuple, 4>; // [1, 2, 3, 4]

type UnshiftArr<Arr extends unknown[], Item> = [Item, ...Arr]; // 在数组头部添加元素
type UnshiftResult = UnshiftArr<tuple, 0>; // [0, 1, 2, 3]

type tuple1 = [1, 2];
type tuple2 = ["hello", "world"];

type ZipArr<Arr1 extends [unknown, unknown], Arr2 extends [unknown, unknown]> = Arr1 extends [
  infer OneFirst,
  infer OneSecond
]
  ? Arr2 extends [infer TwoFirst, infer TwoSecond]
    ? [[OneFirst, TwoFirst], [OneSecond, TwoSecond]]
    : []
  : [];
type ZipResult = ZipArr<tuple1, tuple2>; // [[1, 'hello'], [2, 'world']]

# 字符串类型

字符串类型的重新构造:从已有的字符串类型中提取出一些部分字符串,经过一系列变换,构造成新的字符串类型。

代码语言:javascript复制
type str = "hello world";

type CapitalizeStr<Str extends string> = Str extends `${infer First}${infer Rest}`
  ? `${Uppercase<First>}${Rest}`
  : Str; // Uppercase 是内置类型,把字符串转换为大写

type CapitalizeResult = CapitalizeStr<str>; // "Hello world"

type str2 = "hello_world";

type CamelCaseStr<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}`
  ? `${Left}${Uppercase<Right>}${CamelCaseStr<Rest>}`
  : Str;

type CamelCaseResult = CamelCaseStr<str2>; // "helloWorld"

type str3 = "helloWorld, helloWorld, helloTypeScript";

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

type DropSubStrResult = DropSubStr<str3, "hello">; // "World, World, TypeScript"

# 函数类型

代码语言:javascript复制
type Func = (a: number, b: string) => boolean;

type AppendArg<Func extends Function, Arg> = Func extends (...args: infer Args) => infer ReturnType
  ? (...args: [...Args, Arg]) => ReturnType
  : never;

type AppendArgResult = AppendArg<Func, boolean>; // (a: number, b: string, c: boolean) => boolean

# 索引类型

索引类型是聚合多个元素的类型class、对象等都是索引类型。

代码语言:javascript复制
type obj = {
  name: string;
  age: number;
};

// Mapping
type Mapping<Obj extends object> = {
  [Key in keyof Obj]: [Obj[Key], Obj[Key], Obj[Key]];
};

type MappingResult = Mapping<obj>; // { name: [string, string, string]; age: [number, number, number] }

type UppercaseKey<Obj extends object> = {
  [Key in keyof Obj as Uppercase<Key & string>]: Obj[Key];
}; // Key & string 约束 Key 必须是 string 类型

type UppercaseKeyResult = UppercaseKey<obj>; // { NAME: string; AGE: number }

// Record - TypeScript 提供了内置的高级类型 Record 来创建索引类型
// type Record<K extends string | number | symbol, T> = {
//   [P in K]: T;
// };

type RecordResult = Record<"name" | "age", string>; // { name: string; age: string }

type ToReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ToReadonlyResult = ToReadonly<obj>; // { readonly name: string; readonly age: number }

type ToPartial<T> = {
  [P in keyof T]?: T[P];
};

type ToPartialResult = ToPartial<obj>; // { name?: string; age?: number }

type ToMutable<T> = {
  -readonly [P in keyof T]: T[P];
};

type ToMutableResult = ToMutable<ToReadonlyResult>; // { name: string; age: number }

type ToRequired<T> = {
  [P in keyof T]-?: T[P];
};

type ToRequiredResult = ToRequired<ToPartialResult>; // { name: string; age: number }

type ToPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type ToPickResult = ToPick<obj, "name">; // { name: string }

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

type FilterByValueTypeResult = FilterByValueType<obj, string>; // { name: string }

TypeScript 支持 typeinfer类型参数 来保存任意类型,相当于变量的作用。

但其实也不能叫变量,因为它们是不可变的。想要变化就需要重新构造新的类型,并且可以在构造新类型的过程中对原类型做一些过滤和变换。

数组、字符串、函数、索引类型等都可以用这种方式对原类型做变换产生新的类型。其中索引类型有专门的语法叫做映射类型,对索引做修改的 as 叫做重映射

# 递归复用

递归

递归是把问题分解为一系列相似的小问题,通过函数不断调用自身来解决这一个个小问题,直到满足结束条件,就完成了问题的求解。

TypeScript 的高级类型支持类型参数,可以做各种类型运算逻辑,返回新的类型,和函数调用是对应的,自然也支持递归。

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

# Promise

代码语言:javascript复制
type p = Promise<Promise<Promise<Record<string, any>>>>;

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

type DeepPromiseValueTypeResult = DeepPromiseValueType<p>; // { [key: string]: any }

type DeepPromiseValueType2<P> = P extends Promise<infer ValueType>
  ? DeepPromiseValueType2<ValueType>
  : P;

type DeepPromiseValueTypeResult2 = DeepPromiseValueType2<p>; // { [key: string]: any }

# 数组类型

代码语言:javascript复制
type arr = [1, 2, 3, 4, 5];

type ReverseArr<Arr extends unknown[]> = Arr extends [infer Head, ...infer Tail]
  ? [...ReverseArr<Tail>, Head]
  : Arr;

type ReverseArrResult = ReverseArr<arr>; // [5, 4, 3, 2, 1]

type IsEqual<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;

type ArrIncludes<Arr extends unknown[], Target> = Arr extends [infer Cursor, ...infer Rest]
  ? IsEqual<Cursor, Target> extends true
    ? true
    : ArrIncludes<Rest, Target>
  : false;

type ArrIncludesResult = ArrIncludes<arr, 3>; // true
type ArrIncludesResult2 = ArrIncludes<arr, 6>; // false
type ArrIncludesResult3 = ArrIncludes<[], 1>; // false

type RemoveItem<Arr extends unknown[], Target, Result extends unknown[] = []> = Arr extends [
  infer Cursor,
  ...infer Rest
]
  ? IsEqual<Cursor, Target> extends true
    ? RemoveItem<Rest, Target, Result>
    : RemoveItem<Rest, Target, [...Result, Cursor]>
  : Result;

type RemoveItemResult = RemoveItem<arr, 3>; // [1, 2, 4, 5]
type RemoveItemResult2 = RemoveItem<[1, 1, 2, 1, 3], 1>; // [2, 3]

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

type BuildArrayResult = BuildArray<5, "a">; // ["a", "a", "a", "a", "a"]

# 字符串类型

代码语言:javascript复制
type ReplaceStr<
  Str extends string,
  From extends string,
  To extends string
> = Str extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${Tail}` : Str;

type ReplaceStrResult = ReplaceStr<"hello world", "world", "typescript">; // "hello typescript"

type ReplaceAll<
  Str extends string,
  From extends string,
  To extends string
> = Str extends `${infer Head}${From}${infer Tail}`
  ? `${Head}${To}${ReplaceAll<Tail, From, To>}`
  : Str;

type ReplaceAllResult = ReplaceAll<"hello world", "l", "L">; // "heLLo worLd"

type StringToUnion<Str extends string> = Str extends `${infer Head}${infer Tail}`
  ? Head | StringToUnion<Tail>
  : never;

type StringToUnionResult = StringToUnion<"hello">; // "h" | "e" | "l" | "o"

type ReverseStr<
  Str extends string,
  Result extends string = ""
> = Str extends `${infer Head}${infer Tail}` ? ReverseStr<Tail, `${Head}${Result}`> : Result;

type ReverseStrResult = ReverseStr<"hello">; // "olleh"

# 对象类型

代码语言:javascript复制
type obj = {
  name: string;
  address: {
    city: string;
    country: string;
  };
  say: () => void;
};

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

type DeepReadonlyResult = DeepReadonly<obj>;
// type DeepReadonlyResult = {
//     readonly name: string;
//     readonly address: DeepReadonly<{
//         city: string;
//         country: string;
//     }>;
//     readonly say: () => void;
// }

// 注意 address 的值并没有触发计算,因为 TS 的类型只有被用到才会被计算

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

type DeepReadonlyResult2 = DeepReadonly2<obj>;
// type DeepReadonlyResult2 = {
//     readonly name: string;
//     readonly address: {
//         readonly city: string;
//         readonly country: string;
//     };
//     readonly say: () => void;
// }

在 TypeScript 类型系统中的高级类型也同样支持递归,在类型体操中,遇到数量不确定的问题,要条件反射的想到递归。 比如数组长度不确定、字符串长度不确定、索引类型层数不确定等。

# 数值计算

TypeScript 类型系统中没有加减乘除运算符,但是可以通过构造不同的数组然后取 length 的方式来完成数值计算,把数值的加减乘除转化为对数组的提取和构造。

代码语言:javascript复制
type num1 = [1, 2, 3, 4, 5]["length"]; // 5

type num2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]["length"]; // 10

# 数组长度实现加减乘除

代码语言:javascript复制
type BuildArray<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr["length"] extends Length ? Arr : BuildArray<Length, Ele, [...Arr, Ele]>;

type BuildArrayResult = BuildArray<5, "a">; // ["a", "a", "a", "a", "a"]
type num1 = BuildArrayResult["length"]; // 5

type Add<Num1 extends number, Num2 extends number> = [
  ...BuildArray<Num1>,
  ...BuildArray<Num2>
]["length"];

type num2 = Add<5, 10>; // 15

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

type num3 = Substract<10, 5>; // 5

type Multiply<
  Num1 extends number,
  Num2 extends number,
  Result extends unknown[] = []
> = Num2 extends 0
  ? Result["length"]
  : Multiply<Num1, Substract<Num2, 1>, [...Result, ...BuildArray<Num1>]>;

type num4 = Multiply<5, 10>; // 50

type Divide<Num1 extends number, Num2 extends number, Count extends unknown[] = []> = Num1 extends 0
  ? Count["length"]
  : Divide<Substract<Num1, Num2>, Num2, [...Count, 1]>;

type num5 = Divide<10, 5>; // 2
type num6 = Divide<2, 5>; // never

//  add(num1, num2) {
//    return num1   num2;
//  }
//  substract(num1, num2) {
//    return num1 - num2;
//  }
//  multiply(num1, num2, res = 0) {
//    if (num2 === 0) {
//      return res;
//    }
//    return multiply(num1, num2 - 1, res   num1);
//  }
//  divide(num1, num2, count = 0) {
//    if (num1 === 0 || num1 < num2) {
//      return count;
//    }
//    return divide(num1 - num2, num2, count   1);
//  }

# 数组长度实现计数

代码语言:javascript复制
type StrLen<Str extends string, Count extends unknown[] = []> = Str extends `${string}${infer Rest}`
  ? StrLen<Rest, [...Count, 1]>
  : Count["length"];

type StrLenResult = StrLen<"hello">; // 5

type GreaterThan<
  Num1 extends number,
  Num2 extends number,
  Count extends unknown[] = []
> = Num1 extends Num2
  ? false
  : Count["length"] extends Num2
  ? true
  : Count["length"] extends Num1
  ? false
  : GreaterThan<Num1, Num2, [...Count, 1]>;

type isGreater1 = GreaterThan<5, 10>; // false
type isGreater2 = GreaterThan<10, 5>; // true

// greaterThan(num1, num2, count = 0) {
//   if (num1 === num2) {
//     return false;
//   }
//   if (count === num2) {
//     return true;
//   }
//   if (count === num1) {
//     return false;
//   }
//   return greaterThan(num1, num2, count   1);
// }

# 联合类型

当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型

代码语言:javascript复制
type Union = "a" | "b" | "c";

type UppercaseItem<Items extends string, Target extends string> = Items extends Target
  ? Uppercase<Items>
  : Items;

type UppercaseUnion = UppercaseItem<Union, "a">; // "A" | "b" | "c"
type UppercaseUnion2 = UppercaseItem<Union, "b">; // "a" | "B" | "c"

type AddPrefix<Items extends string, Prefix extends string> = `${Prefix}${Items}`;

type AddPrefixUnion = AddPrefix<Union, "prefix_">; // "prefix_a" | "prefix_b" | "prefix_c"

TypeScript 对联合类型在条件类型中使用时的特殊处理:会把联合类型的每一个元素单独传入做类型计算,最后合并。

# CamelcaseUnion

代码语言:javascript复制
type CamelCaseStr<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}`
  ? `${Left}${Uppercase<Right>}${CamelCaseStr<Rest>}`
  : Str;

type CamelCaseStrResult = CamelCaseStr<"hello_world">; // "helloWorld"

type CamelcaseArr<Arr extends unknown[]> = Arr extends [infer First, ...infer Rest]
  ? [CamelCaseStr<First & string>, ...CamelcaseArr<Rest>]
  : Arr;

type CamelcaseArrResult = CamelcaseArr<["hello_world", "hello_ts"]>; // ["helloWorld", "helloTs"]

type CamelcaseUnion<Item extends string> = Item extends `${infer Left}_${infer Right}${infer Rest}`
  ? `${Left}${Uppercase<Right>}${CamelcaseUnion<Rest>}`
  : Item;

type CamelcaseUnionResult = CamelcaseUnion<"hello_world" | "hello_ts">; // "helloWorld" | "helloTs"

# IsUnion

代码语言:javascript复制
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;

type IsUnionResult = IsUnion<"a" | "b">; // true
type IsUnionResult2 = IsUnion<"a">; // false

条件类型中如果左边的类型是联合类型,会把每个元素单独传入做计算,而右边不会。

代码语言:javascript复制
type TestUnion<A, B = A> = A extends A ? { a: A; b: B } : never;

type TestUnionResult = TestUnion<"a" | "b" | "c">;

// type TestUnionResult = {
//   a: "a";
//   b: "a" | "b" | "c";
// } | {
//   a: "b";
//   b: "a" | "b" | "c";
// } | {
//   a: "c";
//   b: "a" | "b" | "c";
// }

当 `A` 是联合类型时

  • A extends A 这种写法是为了触发分布式条件类型,让每个类型单独传入处理的,没别的意义。
  • A extends A[A] extends [A] 是不同的处理,前者是单个类型和整个类型做判断,后者两边都是整个联合类型,因为只有 extends 左边直接是类型参数才会触发分布式条件类型。

# BEM

BEM 是 css 命名规范,用 block__element--modifier 的形式来描述某个区块下面的某个元素的某个状态的样式。

代码语言:javascript复制
type BEM<
  Block extends string,
  Element extends string[],
  Modifier extends string[]
> = `${Block}__${Element[number]}--${Modifier[number]}`;

type bemResult = BEM<"msg", ["title", "content"], ["red", "bold"]>;

// type bemResult = "msg__title--red"
// | "msg__title--bold"
// | "msg__content--red"
// | "msg__content--bold"

# AllCombinations

实现一个全组合的高级类型,传入 'A' | 'B' 的时候,能够返回所有的组合: 'A' | 'B' | 'BA' | 'AB'

代码语言:javascript复制
type Combination<A extends string, B extends string> = A | B | `${A}${B}` | `${B}${A}`;

type CombinationResult = Combination<"A", "B">;
// "A" | "B" | "AB" | "BA"

type AllCombinations<A extends string, B extends string = A> = A extends A
  ? Combination<A, AllCombinations<Exclude<B, A>>>
  : never;

type AllCombinationsResult = AllCombinations<"A" | "B" | "C">;
// "A" | "B" | "C" | "AB" | "AC" | "BA" | "BC" | "CA" | "CB" | "ABC" | "ACB" | "BAC" | "BCA" | "CAB" | "CBA"

联合类型中的每个类型都是相互独立的,TypeScript 对它做了特殊处理,也就是遇到字符串类型、条件类型的时候会把每个类型单独传入做计算,最后把每个类型的计算结果合并成联合类型。

# 特殊类型

类型的判断要根据它的特性来,比如判断联合类型就要根据它的 distributive 的特性。

# IsAny

any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any

代码语言:javascript复制
type IsAny<T> = 0 extends 1 & T ? true : false;

type IsAnyResult = IsAny<any>; // true
type IsAnyResult2 = IsAny<unknown>; // false

# IsEqual

代码语言:javascript复制
type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
  ? true
  : false;

type IsEqualResult = IsEqual<"a", "a">; // true
type IsEqualResult2 = IsEqual<"a", "b">; // false
type IsEqualResult3 = IsEqual<any, "a">; // false
type IsEqualResult4 = IsEqual<any, any>; // true ??

# IsUnion

判断 union 类型,要根据它遇到条件类型时会分散成单个传入做计算的特性:

代码语言:javascript复制
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;

type IsUnionResult = IsUnion<"a" | "b">; // true
type IsUnionResult2 = IsUnion<"a">; // false

# IsNever

never 在条件类型中也比较特殊,如果条件类型左边是类型参数,并且传入的是 never,那么直接返回 never

代码语言:javascript复制
type TestNever<T> = T extends number ? 1 : 2;

type TestNeverResult = TestNever<never>; // never

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

type IsNeverResult = IsNever<never>; // true

# IsTuple

元组类型的 length 是数字字面量,而数组的 lengthnumber

代码语言:javascript复制
type NotEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
  ? false
  : true;

type IsTuple<T> = T extends [...params: infer Eles] ? NotEqual<Eles["length"], number> : false;

type IsTupleResult = IsTuple<[1, 2, 3]>; // true
type IsTupleResult2 = IsTuple<number[]>; // false

# UnionToIntersection

类型之间是有父子关系的,更具体的那个是子类型,比如 AB 的交叉类型 A & B 就是联合类型 A | B 的子类型,因为更具体。

如果允许父类型赋值给子类型,就叫做逆变

如果允许子类型赋值给父类型,就叫做协变

在 TypeScript 中有函数参数是有逆变的性质的,也就是如果参数可能是多个类型,参数类型会变成它们的交叉类型。

代码语言:javascript复制
type UnionToIntersection<U> = (U extends U ? (x: U) => unknown : never) extends (
  x: infer R
) => unknown
  ? R
  : never;
// U extends U 是为了触发联合类型的 distributive 的性质,让每个类型单独传入做计算,最后合并

type UnionToIntersectionResult = UnionToIntersection<
  | {
      name: string;
    }
  | {
      age: number;
    }
>;
// { name: string; } & { age: number; }

# GetOptional

如何提取索引类型中的可选索引呢?利用可选索引的特性:可选索引的值为 undefined 和值类型的联合类型。

代码语言:javascript复制
type GetOptional<Obj extends Record<string, any>> = {
  [Key in keyof Obj as {} extends Pick<Obj, Key> ? Key : never]: Obj[Key];
};

type GetOptionalResult = GetOptional<{
  name: string;
  age?: number;
}>;
// type GetOptionalResult = {
//     age?: number | undefined;
// }

用映射类型的语法重新构造索引类型,索引是之前的索引也就是 Key in keyof Obj,但要做一些过滤,也就是 as 之后的部分。

Pick 是 ts 提供的内置高级类型,就是取出某个 Key 构造新的索引类型:

代码语言:javascript复制
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

可选的意思是这个索引可能没有,没有的时候,那 Pick<Obj, Key> 就是空的,所以 {} extends Pick<Obj, Key> 就能过滤出可选索引。

# GetRequired

代码语言:javascript复制
type isRequired<Key extends keyof Obj, Obj> = {} extends Pick<Obj, Key> ? never : Key;

type isRequiredResult = isRequired<"name", { name: string; age?: number }>; // "name"

type GetRequired<Obj extends Record<string, any>> = {
  [Key in keyof Obj as isRequired<Key, Obj>]: Obj[Key];
};

type GetRequiredResult = GetRequired<{
  name: string;
  age?: number;
}>;
// type GetRequiredResult = {
//     name: string;
// }

# RemoveIndexSignature

索引类型可能有索引,也可能有可索引签名:

代码语言:javascript复制
type Person = {
  [key: string]: any; // 可索引签名,即可以添加任意个 string 类型的索引
  say(): void; // 具体索引
};

索引签名不能构造成字符串字面量类型,因为它没有名字,而其他索引可以。

代码语言:javascript复制
type RemoveIndexSignature<Obj extends Record<string, any>> = {
  [Key in keyof Obj as Key extends `${infer Str}` ? Str : never]: Obj[Key];
};

type RemoveIndexSignatureResult = RemoveIndexSignature<{
  [key: string]: any;
  say(): void;
}>;
// type RemoveIndexSignatureResult = {
//     say: () => void;
// }

# ClassPublicProps

如何过滤出 classpublic 的属性呢?

根据它的特性:keyof 只能拿到 classpublic 索引,privateprotected 的索引会被忽略。

代码语言:javascript复制
class Person {
  public name: string;
  protected id: number;
  private age: number;

  constructor() {
    this.name = "Cell";
    this.age = 18;
    this.id = 1;
  }
}

type ClassPublicProps<Obj extends Record<string, any>> = {
  [Key in keyof Obj]: Obj[Key];
};

type ClassPublicPropsResult = ClassPublicProps<Person>;
// type ClassPublicPropsResult = {
//     name: string;
// }

# as const

TypeScript 默认推导出来的类型并不是字面量类型:

代码语言:javascript复制
const obj = {
  age: 18,
};

type objType = typeof obj;
// type objType = {
//     age: number;
// }

const arr = [1, 2, 3];

type arrType = typeof arr;
// type arrType = number[]

但是类型编程很多时候是需要推导出字面量类型的,这时候就需要用 as const

代码语言:javascript复制
const obj = {
  age: 18,
} as const;

type objType = typeof obj;
// type objType = {
//     readonly age: 18;
// }

const arr = [1, 2, 3] as const;

type arrType = typeof arr;
// type arrType = readonly [1, 2, 3]

加上 as const 之后推导出来的类型是带有 readonly 修饰的,所以再通过模式匹配提取类型的时候也要加上 readonly 的修饰才行。

const 是常量的意思,也就是说这个变量首先是一个字面量值,而且还不可修改,有字面量和 readonly 两重含义。所以加上 as const 会推导出 readonly 的字面量类型。

# 特殊类型特性

  • any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any,可以用这个特性判断 any 类型。
  • 联合类型作为类型参数出现在条件类型左侧时,会分散成单个类型传入,最后合并。
  • never 作为类型参数出现在条件类型左侧时,会直接返回 never
  • any 作为类型参数出现在条件类型左侧时,会直接返回 trueTypefalseType 的联合类型。
  • 元组类型也是数组类型,但 length 是数字字面量,而数组的 lengthnumber。可以用来判断元组类型。
  • 函数参数处会发生逆变,可以用来实现联合类型转交叉类型。
  • 可选索引的索引可能没有,那 Pick 出来的就可能是 {},可以用来过滤可选索引,反过来也可以过滤非可选索引。
  • 索引类型的索引为字符串字面量类型,而可索引签名不是,可以用这个特性过滤掉可索引签名。
  • keyof 只能拿到 classpublic 的索引,可以用来过滤出 public 的属性。
  • 默认推导出来的不是字面量类型,加上 as const 可以推导出字面量类型,但带有 readonly 修饰,这样模式匹配的时候也得加上 readonly 才行。

0 人点赞