# 内置工具类型进阶
# 属性修饰
深层属性修饰:
代码语言:javascript复制// 递归的工具类型
type PromiseValue<T> = T extends Promise<infer U> ? PromiseValue<U> : T;
对于 Partial
和 Required
:
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
的工具类型,可以借助其实现一个剔除所有属性的 null
与 undefined
:
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]
的理解:
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
进行封装,将预期类型也作为泛型参数:
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
处理大部分时候也是通过深层嵌套实现:
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
和 模板字符串类型,可以基于已有对象类型来实现精确到字面量的类型推导:
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
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
工具类型:
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
工具类型:
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
工具类型:
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'