TS 进阶 - 类型编程

2023-05-17 20:19:03 浏览数 (4)

# 内置工具类型进阶

# 属性修饰

深层属性修饰:

代码语言:javascript复制
// 递归的工具类型
type PromiseValue<T> = T extends Promise<infer U> ? PromiseValue<U> : T;

对于 PartialRequired:

代码语言:javascript复制
export type DeepPartial<T extends object> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

使用 tsd 工具类型单元测试库验证:

代码语言:javascript复制
import { expectType } from 'tsd';

type DeepPartialStruct = DeepPartial<{
  foo: string;
  nested: {
    nestedFoo: string;
    nestedBar: {
      nestedBarFoo: string;
    };
  };
}>;

expectType<DeepPartialStruct>({
  foo: 'foo',
  nested: {
  },
});

expectType<DeepPartialStruct>({
  nested: {
    nestedBar: {},
  },
});

expectType<DeepPartialStruct>({
  nested: {
    nestedBar: {
      nestedBarFoo: undefined,
    },
  },
});

其他递归属性修饰工具类型:

代码语言:javascript复制
export type DeepPartial<T extends object> = {
  [K in keyof T]: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

export type DeepRequired<T extends object> = {
  [K in keyof T]-?: T[k] extends object ? DeepRequired<T[K]> : T[K];
};

export type DeepReadonly<T extends object> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

export type DeepWritable<T extends object> = {
  -readonly [K in keyof T]: T[K] extends object ? DeepWritable<T[K]> : T[K];
};

内置工具类型中有一个从联合类型中提出 null | undefined 的工具类型,可以借助其实现一个剔除所有属性的 nullundefined

代码语言:javascript复制
type NonNullable<T> = T extends null | undefined ? never : T;

export type DeepNonNullable<T extends object> = {
  [K in keyof T]: T[K] extends object
    ? DeepNonNullable<T[K]>
    : NonNullable<T[K]>;
};

// 对应的 Nullable
export type Nullable<T> = T | null;

export type DeepNullable<T extends object> = {
  [K in keyof T]: T[K] extends object
    ? DeepNullable<T[K]>
    : Nullable<T[K]>;
}

基于已知属性进行部分修饰,如让一个对象的一部分已知属性变成可选的,只要将该对象拆为 A 和 B 两个对象结构,分别由已知属性和其他属性组成,然后将 A 的属性全变为可选,再和对象 B 进行组合。

使用最广泛的一种类型编程思路:将复杂的工具类型,拆解为由基础工具类型、类型工具的组合

代码语言:javascript复制
export type MarkPropsAsOptional<
  T extends object, // T 为要处理的对象
  K extends keyof T = keyof T // K 为需要标记为可选的属性,默认值为 T 的所有属性
> = Partial<Pick<T, K>> // 标记为可选属性组成的对象结构
  & Omit<T, K>; // 不需要处理的那部分属性组成的对象结构

type MarkPropsAsOptionalStruct = MarkPropsAsOptional<
  {
    foo: string,
    bar: number,
    baz: boolean,
  },
  'bar'
>;

辅助工具 Flatten,用于将交叉类型结构展平为单层的对象结构:

代码语言:javascript复制
export type Flatten<T> = {
  [K in keyof T]: T[K];
};

export type MarkPropsAsOptional<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Partial<Pick<T, K>> & Omit<T, K>>;

一些其他的类型的部分修饰:

代码语言:javascript复制
export type MarkPropsAsAsRequired<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Required<Pick<T, K>>>;

export type MarkPropsAsReadonly<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Readonly<Pick<T, K>>>;

export type MarkPropsAsMutable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Mutable<Pick<T, K>>>;

export type MarkPropsAsNonNullable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & NonNullable<Pick<T, K>>>;

export type MarkPropsAsNullable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Nullable<Pick<T, K>>>;

# 结构工具类型

基于期望的类型去拿到所有此类型的属性名:

代码语言:javascript复制
type FuncStruct = (...args: any[]) => any;

type FunctionKeys<T extends object> = {
  [K in keyof T]: T[K] extends FuncStruct ? K : never;
}[keyof T];

对于 {}[key of T] 的理解:

代码语言:javascript复制
type Tmp<T extends object> = {
  [K in keyof T]: T[K] extends FuncStruct ? K : never;
};

type Res = Tmp<{
  foo: () => {};
  bar: () => string;
  baz: string;
}>;

// 等价于
// type Res = {
//   foo: 'foo';
//   bar: 'bar';
//   baz: never;
// };

// 使用 [keyof T] 索引类型查询
type WhatWillWeGet = Res[keyof Res]; // 'foo' | 'bar'

如果希望抽象“基于键值类型查找属性名”,可以对 FunctionKeys 进行封装,将预期类型也作为泛型参数:

代码语言:javascript复制
type ExpectedPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];

type FunctionKeys<T extends object> = ExpectedPropKeys<T, FuncStruct>;

# 集合工具类型

从一维原始类型集合,扩展二维的对象类型,在对象类型之间进行交叉并补集运算,以及对同名属性的各种情况处理。

一维集合:

代码语言:javascript复制
export type Concurrence<A, B> = A | B;

export type Intersection<A, B> = A extends B ? A : never;

export type Difference<A, B> = A extends B ? never : A;

export type Complement<A, B extends A> = Difference<A, B>;

对象属性名的版本:

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

// 属性名并集
export type ObjectKeysConcurrence<
  T extends PlainObjectType,
  U extends PlainObjectType
> = keyof T | keyof U;

// 属性名交集
export type ObjectKeysIntersection<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Intersection<keyof T, keyof U>;

// 属性名差集
export type ObjectKeysDifference<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Difference<keyof T, keyof U>;

// 属性名补集
export type ObjectKeysComplement<
  T extends U,
  U extends PlainObjectType
> = Complement<keyof T, keyof U>;

对于 交集、补集、差集,可以使用属性名的集合来实现对象层面的版本:

代码语言:javascript复制
// 交集
export type ObjectIntersection<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Pick<T, ObjectKeysIntersection<T, U>>;

// 差集
export type ObjectDifference<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Pick<T, ObjectKeysDifference<T, U>>;

// 补集
export type ObjectComplement<
  T extends U,
  U extends PlainObjectType
> = Pick<T, ObjectKeysComplement<T, U>>;

其他复杂的操作:

代码语言:javascript复制
type Merge<
  T extends PlainObjectType,
  U extends PlainObjectType
> = ObjectDifference<T, U> & ObjectIntersection<U, T> & ObjectDifference<U, T>;

type Assign<
  T extends PlainObjectType,
  U extends PlainObjectType
> = ObjectDifference<T, U> & ObjectIntersection<T, U> & ObjectDifference<U, T>;

type Override<
  T extends PlainObjectType,
  U extends PlainObjectType
  > = ObjectDifference<T, U> & ObjectIntersection<U, T>;

# 模式匹配工具类型

模式匹配工具类型的进阶只有深层嵌套,特殊位置的 infer 处理大部分时候也是通过深层嵌套实现:

代码语言:javascript复制
type FirstParameter<T extends Function> = T extends (
  arg: infer P,
  ...args: any
) => any ? P : never;

无论多复杂的类型编程,最终都可以拆分为数个基础的工具类型来实现。

# 模板字符串类型

# 基础使用

代码语言:javascript复制
type World = 'World';

type Greeting = `Hello ${World}`; // 'Hello World'

Greeting 是一个模板字符串类型,内部通过与 JavaScript 中模板字符串相同的语法${},使用了另一个类型别名 Wordl,其最终的类型就是将两个字符串类型值组装在一起返回。

除了使用确定的类型别名以外,模板字符串类型也支持通过泛型参数传入。注意,并不是所有值度能被作为模板插槽:

代码语言:javascript复制
type Greet<T extends string | number | boolean | null | undefined | bigint> = `Hello ${T}`;

type Greet1 = Greet<'Cell'>; // 'Hello Cell'
type Greet2 = Greet<123>; // 'Hello 123'
type Greet3 = Greet<true>; // 'Hello true'
type Greet4 = Greet<null>; // 'Hello null'
type Greet5 = Greet<undefined>; // 'Hello undefined'
type Greet6 = Greet<0x1fffffffffffff>; // 'Hello 9007199254740991'

也可以直接为插槽传入一个类型而非类型别名:

代码语言:javascript复制
type Greeting = `Hello ${string}`;
// 这种情况下 Greeting 类型并不会变成 `Hello string`,而是保持 `Hello ${string}`。
// 此时是一个无法改变的模板字符串类型,但所有 `Hello ` 开头的字面量类型都会是其子类型

模板字符串类型的主要目的是增强字符串字面量类型的灵活性,进一步增强类型和逻辑代码的关联。

通过模板字符串类型声明版本号:

代码语言:javascript复制
type Version = `${number}.${number}.${number}`;

const v1: Version = '1.0.0';

const v2: Version = '1.0'; // Error: '1.0' 不是 Version 类型

通过模板字符类型减少代码的同时获得更好的类型保障:

代码语言:javascript复制
type Brand = 'iphone' | 'huawei' | 'xiaomi';
type Memory = '16G' | '32G' | '64G' | '128G';
type isSecondHand = 'new' | 'secondHand';

type SKU = `${Brand}-${Memory}-${isSecondHand}`;

通过泛型传入联合类型时,也会有分发过程:

代码语言:javascript复制
type SizeRecord<Size extends string> = `${Size}-Record`;

type Size = 'S' | 'M' | 'L';

type SizeRecordUnion = SizeRecord<Size>; // 'S-Record' | 'M-Record' | 'L-Record'

# 类型表现

由于模板字符串类型最终产物还是字符串字面量类型,因此只要插槽位置的类型匹配,字符串字面量类型就可以被认为是模板字符串类型的子类型:

代码语言:javascript复制
declare let v1: `${number}.${number}.${number}`;
declare let v2: '1.0.0';

v1 = v2; // OK
v2 = v1; // ERROR '`${number}.${number}.${number}`' is not assignable to type '"1.0.0"'.

通过模板字符串类型,可以更精确进行类型描述:

代码语言:javascript复制
const greet = (to: string): `Hello ${string}` => `Hello ${to}`;

# 结合索引类型与映射类型

基于 keyof 和 模板字符串类型,可以基于已有对象类型来实现精确到字面量的类型推导:

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

type ChangeListener = {
  on: (change: `${keyof Foo} Changed`) => void;
};

declare let listener: ChangeListener;

listener.on('name Changed'); // OK
listener.on('age Changed'); // OK
listener.on('job Changed'); // OK
listener.on('foo Changed'); // ERROR 'foo Changed' is not assignable to 'name Changed' | 'age Changed' | 'job Changed'.

为了与映射类型实现更好的协作,TypeScript 在引入模板字符串类型时支持了一个叫重映射的新语法,基于模板字符串类型与重映射,可以实现:在映射键名时基于原键名做修改:

代码语言:javascript复制
// 通过 as 语法,将映射的键名作为变量,映射到一个新的字符串类型
// 注意模板字符串类型插槽不支持 symbol,需要确保键名是 string
type CopyWithRename<T extends object> = {
  [K in keyof T as `modified_${string & K}`]: T[K];
};

type Foo = {
  name: string;
  age: number;
};

type FooModified = CopyWithRename<Foo>;
// {
//   modified_name: string;
//   modified_age: number;
// }

# 专用工具类型

  • Uppercase
  • Lowercase
  • Capitalize
  • Uncapitalize
代码语言:javascript复制
type Heavy<T extends string> = `${Uppercase<T>}`;
type Respect<T extends string> = `${Capitalize<T>}`;

type HeavyHello = Heavy<'hello'>; // 'HELLO'
type RespectHello = Respect<'hello'>; // 'Hello'

# 模板字符串类型与模式匹配

模式匹配工具类型的核心理念就是对符合约束的某个类型结构,提取其某一个位置的类型,如函数结构中参数与返回值类型。如果将一个字符串类型视为一个结构,就能在其中也应用模式匹配相关的能力:

代码语言:javascript复制
type ReverseName<Str extends string> = Str extends `${infer oldLeft} ${infer oldRight}`
  ? `${oldRight} ${oldLeft}` : Str;

type ReverseName1 = ReverseName<'hello world'>; // 'world hello'
type ReverseName2 = ReverseName<'hello'>; // 'hello'
type ReverseName3 = ReverseName<'hello world !'>; // 'world ! hello'

# 基于重映射的 PickByValueType

代码语言:javascript复制
type PickByValueType<T extends object, Type> = {
  [K in keyof T as T[K] extends Type ? K : never]: T[K];
};

# 模板字符串工具类型进阶

# Trim、Includes

判断传入的字符串字面量类型中是否含有某个字符串:

代码语言:javascript复制
type Include<
  Str extends string,
  Search extends string
> = Str extends `${infer _R1}${Search}${infer _R2}` ? true : false;

type IsHelloWorld = Include<'hello world', 'hello'>; // true
type IsHelloWorld2 = Include<'hello world', 'world'>; // true
type IsHelloWorld3 = Include<'hello world', 'foo'>; // false
type IsInluced1 = Include<'hello world', ''>; // true
type IsInluced2 = Include<' ', ''>; // true
type IsInluced3 = Include<'', ''>; // false

对空字符串进行特殊处理:

代码语言:javascript复制
type _Include<
  Str extends string,
  Search extends string
> = Str extends `${infer _R1}${Search}${infer _R2}` ? true : false;

type Include<
  Str extends string,
  Search extends string
> = Str extends ''
  ? Str extends ''
    ? true
    : false
  : _Include<Str, Search>;

trim 工具类型:

代码语言:javascript复制
type TrimLeft<Str extends string> = Str extends ` ${infer R}` ? R : Str;

type TrimRight<Str extends string> = Str extends `${infer L} ` ? L : Str;

type Trim<Str extends string> = TrimLeft<TrimRight<Str>>;

针对多个空格进行优化:

代码语言:javascript复制
type TrimLeft<Str extends string> = Str extends ` ${infer R}` ? TrimLeft<R> : Str;

type TrimRight<Str extends string> = Str extends `${infer L} ` ? TrimRight<L> : Str;

type Trim<Str extends string> = TrimLeft<TrimRight<Str>>;

# Replace、Split 与 Join

一切复杂的工具类型最终都可以转换为数个简单工具类型的组合。

代码语言:javascript复制
export type Replace<
  Str extends string,
  Search extends string,
  ReplaceStr extends string
> = Str extends `${infer L}${Search}${infer R}`
  ? `${L}${ReplaceStr}${R}`
  : Str;

type ReplaceHelloWorld = Replace<'hello world', 'world', 'foo'>; // 'hello foo'

针对全量替换进行优化:

代码语言:javascript复制
export type ReplaceAll<
  Str extends string,
  Search extends string,
  ReplaceStr extends string
> = Str extends `${infer L}${Search}${infer R}`
  ? ReplaceAll<`${L}${ReplaceStr}${R}`, Search, ReplaceStr>
  : Str;

type ReplaceHelloWorld = ReplaceAll<'hello world', 'l', 'x'>; // 'hexxo worxd'

split 工具类型:

代码语言:javascript复制
export type Split<Str extends string> = 
  Str extends `${infer A}-${infer B}-${infer C}`
    ? [A, B, C]
    : [];

type SplitHelloWorld = Split<'hello-world-foo'>; // ['hello', 'world', 'foo']

优化不确定分隔符和字符串长度:

代码语言:javascript复制
export type Split<
  Str extends string,
  Delimiter extends string
> = Str extends `${infer A}${Delimiter}${infer B}`
  ? [A, ...Split<B, Delimiter>]
  : Str extends Delimiter // 处理字符串只有一个分隔符的情况
  ? []
  : [Str];

type SplitHelloWorld = Split<'hello-world-foo', '-'>; // ['hello', 'world', 'foo']

Join 工具类型:

代码语言:javascript复制
export type Join<
  Arr extends Array<string | number>,
  Delimiter extends string
> = Arr extends []
  ? ''
  : Arr extends [string | number]
    ? `${Arr[0]}`
    : Arr extends [string | number, ...infer R]
      ? `${Arr[0]}${Delimiter}${Join<R, Delimiter>}`
      : string;

type JoinHelloWorld = Join<['hello', 'world', 'foo'], '-'>; // 'hello-world-foo'

1 人点赞