初识 TS 类型体操

123 阅读4分钟

工具类型的实现及实战

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

实现思路

  1. 先观察这个函数的入参和出参的类型

可以看到,入参就是一个字符串,出参是解析入参后生成的 Record,如下所示

// 入参
'a=1&b=2&c=3'

// 出参
{
    a: 1,
    b: 2,
    c: 3
}
  1. 问题拆解

假设入参为 a=1,那么只需要借助 infer 和 Record 构造出新类型即可,代码如下

type ConstructType<T extends string> =
    T extends `${infer Key}=${infer Value}`
    ? Record<Key, Value>
    : Record<string, any>
  1. 逐一实现

有了上面的基础,结合需要处理的数据,可以得知,需要递归进行处理,然后对 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 是一个渐进增强的技术,其效果完全取决于使用者的境界,所以需要多多练习