前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TypeScript 类型体操 - 基础操作

TypeScript 类型体操 - 基础操作

作者头像
Cellinlab
发布2023-05-17 17:02:20
1.7K0
发布2023-05-17 17:02:20
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# 前置知识

# 类型是什么

类型即 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 才行。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022/3/18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 前置知识
    • # 类型是什么
      • # 类型系统
        • # TypeScript 类型系统中的类型
          • # TypeScript 类型系统中的类型运算
          • # 模式匹配
            • # 数组类型
              • # 字符串类型
                • # 函数
                  • # 构造器
                    • # 索引类型
                    • # 重新构造
                      • # 数组类型
                        • # 字符串类型
                          • # 函数类型
                            • # 索引类型
                            • # 递归复用
                              • # Promise
                                • # 数组类型
                                  • # 字符串类型
                                    • # 对象类型
                                    • # 数值计算
                                      • # 数组长度实现加减乘除
                                        • # 数组长度实现计数
                                        • # 联合类型
                                          • # CamelcaseUnion
                                            • # IsUnion
                                              • # BEM
                                                • # AllCombinations
                                                • # 特殊类型
                                                  • # IsAny
                                                    • # IsEqual
                                                      • # IsUnion
                                                        • # IsNever
                                                          • # IsTuple
                                                            • # UnionToIntersection
                                                              • # GetOptional
                                                                • # GetRequired
                                                                  • # RemoveIndexSignature
                                                                    • # ClassPublicProps
                                                                      • # as const
                                                                      • # 特殊类型特性
                                                                      领券
                                                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档