前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TS 进阶 - 类型编程

TS 进阶 - 类型编程

作者头像
Cellinlab
发布2023-05-17 20:19:03
7650
发布2023-05-17 20:19:03
举报
文章被收录于专栏:Cellinlab's Blog

# 内置工具类型进阶

# 属性修饰

深层属性修饰:

代码语言: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'
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/8/5,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 内置工具类型进阶
    • # 属性修饰
      • # 结构工具类型
        • # 集合工具类型
          • # 模式匹配工具类型
          • # 模板字符串类型
            • # 基础使用
              • # 类型表现
                • # 结合索引类型与映射类型
                  • # 专用工具类型
                    • # 模板字符串类型与模式匹配
                      • # 基于重映射的 PickByValueType
                      • # 模板字符串工具类型进阶
                        • # Trim、Includes
                          • # Replace、Split 与 Join
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档