工具类型的实现及实战
Partial<T> 将类型 T 的所有属性变为可选
type TPartial<T> = {
[K in keyof T]?: T[K]
}
type PartialPerson = TPartial<{ name: string; age: number }>;
Required<T> 将类型 T 的所有属性变为必填
type TRequired<T> = {
[K in keyof T]-?: T[K]
}
type RequiredPerson = TRequired<{ name?: string; age?: number }>;
Readonly<T> 将类型 T 的所有属性变为只读
type TReadonly<T> = {
readonly [K in keyof T]: T[K]
}
type ReadonlyPerson = TReadonly<{ name: string; age: number }>;
Record<K, T> 创建一个新类型,该类型具有由 K 中的键组成的属性,并且每个属性的值都是 T 类型
type TRecord<K extends keyof any, T> = {
[P in K]: T
}
type Flags = TRecord<'flagA' | 'flagB', boolean>;
Pick<T, K> 从类型 T 中选择一部分属性,这些属性的键在 K 中
type TPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type PickPerson = TPick<{ name: string; age: number; email: string }, 'name' | 'age'>;
Exclude<T, U> 从类型 T 中排除所有可以赋值给 U 的类型
type TExclude<T, U> = T extends U ? never : T
type ExcludeNumberOrString = TExclude<string | number | boolean, string | number>;
Extract<T, U> 从类型 T 中提取所有可以赋值给 U 的类型
type TExtract<T, U> = T extends U ? T : never
type ExtractNumberOrString = Extract<string | number | boolean, string | number>;
Omit<T, K> 从类型 T 中排除一部分属性,这些属性的键在 K 中
type TPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type TExclude<T, U> = T extends U ? never : T
type TOmit<T, K extends keyof T> = TPick<T, TExclude<keyof T, K>>
type OmitPerson = TOmit<{ name: string; age: number; email: string }, 'email'>;
NonNullable<T> 从类型 T 中排除 null 和 undefined
type TExclude<T, U> = T extends U ? never : T
type TNonNullable<T> = TExclude<T, null | undefined>
type NonNullableType = NonNullable<string | number | null | undefined>;
Parameters<T> 提取函数类型 T 的参数类型
type TParameters<T extends Function> =
T extends (...args: infer R) => void
? R
: never
type Func = (a: string, b: number) => void;
type FuncParams = TParameters<Func>;
ConstructorParameters<T> 提取构造函数类型 T 的参数类型
type TConstructorParameters<T> =
T extends { new (...args: infer R): any }
? R
: never
class Ctor {
constructor(a: string, b: number) {}
}
type CtorParams = TConstructorParameters<typeof Ctor>;
ReturnType<T> 提取函数类型 T 的返回类型
type TReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type Func = (a: string, b: number) => boolean;
type FuncReturnType = TReturnType<Func>;
InstanceType<T> 提取构造函数类型 T 的实例类型
type TInstanceType<T> =
T extends { new (...args: any[]): infer R }
? R
: never
class MyClass {
prop: string;
}
type MyClassInstance = TInstanceType<typeof MyClass>;
ThisParameterType<T> 提取函数类型 T 的 this 参数类型
type TThisParameterType<T extends Function> =
T extends (this: infer R, ...args: any[]) => void
? R
: never
type Func = (this: { a: string }, b: number) => void;
type ThisParamType = TThisParameterType<Func>;
OmitThisParameter<T> 从函数类型 T 中移除 this 参数
type TOmitThisParameter<T> =
T extends (this: any, ...args: infer R) => infer Return
? (...args: R) => Return
: T
type Func = (this: { a: string }, b: number) => void;
type OmitThisFunc = TOmitThisParameter<Func>;
Parameters<T>[index] 提取函数类型 T 的第 index 个参数类型
type TParameters<T extends Function> =
T extends (...args: infer R) => infer Return
? R
: T
type Func = (a: string, b: number) => void;
type FirstParam = TParameters<Func>[0]; // string
ReturnType<ReturnType<T>> 提取嵌套函数的返回类型
type TReturnType<T> =
T extends (...args: any[]) => infer R
? TReturnType<R>
: T
type Func = () => () => string;
type NestedReturnType = TReturnType<Func>; // string
ExtractKeysByType<T, V> 提取类型 T 中值类型为 V 的键
type ExtractKeysByType<T, K extends T[keyof T]> = {
[P in keyof T]: T[P] extends K ? P : never
}[keyof T]
type Obj = { a: string; b: number; c: boolean };
type StringKeys = ExtractKeysByType<Obj, string>; // "a"
ExcludeKeysByType<T, V> 排除类型 T 中值类型为 V 的键
type ExcludeKeysByType<T, K extends T[keyof T]> = {
[P in keyof T]: T[P] extends K ? never : P
}
type Obj = { a: string; b: number; c: boolean };
type NonStringKeys = ExcludeKeysByType<Obj, string>; // "b" | "c"
TAwaited<T> 获取 promise 的 value 类型
type TAwaited<T> =
T extends null | undefined
? T
: T extends object & { then(onFullFilled: infer R): any }
? R extends (value: infer P, ...args: any[]) => any ? TAwaited<P> : never
: T
type TAwaited<T> = T extends Promise<infer U> ? TAwaited<U> : T
type AwaitedRes = TAwaited<Promise<Promise<Promise<string>>>>
实战:为 parseQueryString 函数标记类型
下面是一个解析查询参数的函数,外部在使用 res 时,不会有任何提示,因为默认变量都为 any 类型
function parseQueryString(queryStr) {
if (!queryStr || !queryStr.length) {
return {}
}
const queryObj = {}
const items = queryStr.split('&')
items.forEach((item) => {
const [key, value] = item.split('=')
if (queryObj[key]) {
if (Array.isArray(queryObj[key])) {
queryObj[key].push(value)
} else {
queryObj[key] = [queryObj[key], value]
}
} else {
queryObj[key] = value
}
})
return queryObj
}
const res = parseQueryString('a=1&b=2&c=3');
console.log(res.a) // type 1
实现思路
-
先观察这个函数的入参和出参的类型
可以看到,入参就是一个字符串,出参是解析入参后生成的 Record,如下所示
// 入参
'a=1&b=2&c=3'
// 出参
{
a: 1,
b: 2,
c: 3
}
-
问题拆解
假设入参为 a=1,那么只需要借助 infer 和 Record 构造出新类型即可,代码如下
type ConstructType<T extends string> =
T extends `${infer Key}=${infer Value}`
? Record<Key, Value>
: Record<string, any>
-
逐一实现
有了上面的基础,结合需要处理的数据,可以得知,需要递归进行处理,然后对 ConstructType 的结果进行两两组合,即可得到最终的结果,先实现一个能够将 ConstructType 的结果进行合并的类型,MergeType 接收两个 Record 类型,然后将其合并,代码如下
type MergeType<T extends Record<string, any>, U extends Record<string, any>> = {
[K in keyof T | keyof U]: K extends keyof T ? T[K] : K extends keyof U ? U[k] : never
}
有了以上两个类型后,还需要一个类型对函数的入参使用 infer 进行递归拆解,因为在使用时,可能会输入多个查询参数,代码如下
type ParseString<T extends string> =
T extends `${infer R}&${infer Rest}`
? MergeType<ConstructType<R>, ParseString<Rest>>
: ConstructType<T>
至此,就完成了根据函数的入参进行结果推导,完整代码如下
type ConstructType<T extends string> =
T extends `${infer Key}=${infer Value}`
? Record<Key, Value>
: Record<string, any>
type MergeType<T extends Record<string, any>, U extends Record<string, any>> = {
[K in keyof T | keyof U]: K extends keyof T ? T[K] : K extends keyof U ? U[K] : never
}
type ParseString<T extends string> =
T extends `${infer R}&${infer Rest}`
? MergeType<ConstructType<R>, ParseString<Rest>>
: ConstructType<T>
function parseQueryString<T extends string>(queryStr: T): ParseString<T> {
if (!queryStr || !queryStr.length) return {} as any
const queryObj = {} as any
const items = queryStr.split('&')
items.forEach((item) => {
const [key, value] = item.split('=')
if (queryObj[key]) {
if (Array.isArray(queryObj[key])) {
queryObj[key].push(value)
} else {
queryObj[key] = [queryObj[key], value]
}
} else {
queryObj[key] = value
}
})
return queryObj
}
const res = parseQueryString('a=1&b=2&c=3');
console.log(res.a)
注意,使用 infer 时,必须要完全一致,不能有多余的空格,刚才不小心输入了空格,排查了半天。。。
ts 是一个渐进增强的技术,其效果完全取决于使用者的境界,所以需要多多练习