likes
comments
collection
share

换个角度学TS,也许你能熟悉它

作者站长头像
站长
· 阅读数 4

前言

TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。

一道开胃菜

function memoize<T extends (...args: any[]) => any>(fn: T) {
  const cache = new Map()
  return (...args: Parameters<typeof fn>) => {
    const key = JSON.stringify(fn)
    if (cache.has(key)) {
      return cache.get(key)
    }
    const result = fn(...args)
    cache.set(key, result)
    return result
  }
}

const add = (a: number, b: number) => a + b
const memoAdd = memoize(add)
console.log(memoAdd(1, 2)) // 3
console.log(memoAdd(1, 2)) // 3

上面这个缓存函数的有个泛型T, 泛型T被约束为是一个可以是任意类型参数和返回任意类型的函数, 函数被调用时才明确知道T的类型, 比如下面的add函数, 这个时候TS类型就自动推导T是一个a、b参数类型为number返回一个number的函数, 我们看缓存函数内部返回一个函数接收的参数类型应该和T的参数类型一致, 这个时候可以使用TS内置的工具类型Parameters, 它可以返回一个函数类型的参数数组类型, 所以我们typeof fn, 就可以拿到fn的类型, 或者直接使用Parameters<T>。

我们来看看Parameters是怎么实现的:

type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

Parameters工具类型接收一个泛型T被约束为可以是任意的函数类型, 后面用了infer类型推导, 可以在类型被使用时推导出具体的类型, T如果是(...args: infer P) => any的子类型, 就返回P, 否则返回never(表示永不可达)。

不熟悉这种语法没关系, 我们把TS的内置类型都实现一遍,熟能生巧。

TS内置类型工具

Awaited

// 基础用法
type promise = Promise<string>
type p = Awaited<promise> // string
// 定义一个返回 Promise 的函数
function fetchData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('成功啦啦啦');
    }, 1000);
  });
}
// 使用 Awaited 获取 Promise 结果的类型
type ResultType = Awaited<ReturnType<typeof fetchData>>;

const result: ResultType = 'Awaited'; // 此处类型会被推断为 string

async function useResult() {
  const data = await fetchData();
  console.log(data); // 此处 data 的类型已经被推断为 string
}
useResult();

这里的ReturnType和Parameters是配对的, 一个是获取函数类型的参数类型, 一个是获取函数类型的返回值类型, 我们看看ReturnType是怎么实现的

type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

我们发现和上面Parameters的实现如出一辙, 只是把infer推断从参数的位置移动到了函数返回值的位置。不过ReturnType如果不满足条件返回的是any而不是never了, 这并没有什么影响。所以ReturnType<typeof fetchData>拿到的类型是定义promise函数的返回类型Promise<string>, 而我们的Awaited就是要拿到Promise里面的类型string

这里有个思路

type MyAwait<T> = T extends Promise<infer P> ? P : never
type p = MyAwait<Promise<string>> // string

利用infer推断Promise里面的类型, 好的, 恭喜你, 你已经了解TS了, 可是这并不全面, 如果Promise返回的还是一个Promise呢

type MyAwait<T> = T extends Promise<infer P> ? P : never
type p = MyAwait<Promise<Promise<string>>> // Promise<string>

递归?如果P还是一个Promise就递归调用MyAwait直到拿到最里面的类型, 没错就是你想的这样, 非常完美

type MyAwait<T> = T extends Promise<infer P> // T如果是Promise<infer P>的子类型
  ? P extends Promise<unknown> // 如果推断出来的P还是一个Promise<unknown>
    ? MyAwait<P> // 递归MyAwait<P>
    : P // 不是Promise就直接返回P
  : T; // 如果泛型传的都不是一个promise直接返回T
type p = MyAwait<Promise<Promise<string>>>; // string

我们来看看TS内部是如何实现的

type Awaited<T> = T extends null | undefined
  ? T // 对于 `null | undefined` 这种特殊情况,当未启用 `--strictNullChecks` 时,直接返回 T
  : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } 
    // 仅当类型 T 是一个对象并且具有可调用 `then` 方法时,才会进行下一步处理
    ? F extends (value: infer V, ...args: infer _) => any 
      // 如果 `then` 方法的参数是可调用的,提取其第一个参数的类型
      ? Awaited<V> // 递归地解开该值的嵌套异步类型
      : never // `then` 方法的参数不可调用
    : T; // 非对象或不具有 `then` 方法的类型

Partial

// 基础用法
type obj = {
  a: 1,
  b: 2
}
type obj2 = Partial<obj>
/**
 * type obj2 = {
    a?: 1 | undefined;
    b?: 2 | undefined;
   }
*/

我们来实现实现,上面我们讲的是infer提取类型如果有深层的话就是extends配合三元递归,这个Partial就可以用映射成新类型?:表示的就是可选, 可选后除了原来的类型还联合了undefined类型, 这是为了类型安全考虑可选后就可能没有值。

// 基础用法
type obj = {
  a: 1,
  b: 2
}
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}
type obj2 = MyPartial<obj>
/**
 * type obj2 = {
    a?: 1 | undefined;
    b?: 2 | undefined;
    }
*/

原来的obj类型被构造成了一个新的类型obj2,还是一个对象, 对象里面的每一个键(K)是(in)对象里面所有的键(keyof T)?:(可选) T[K](T对象里面的K键的值),反复想想是不是这回事。

如果有多个对象嵌套,就递归

type obj = {
  a: 1,
  b: {
    c: 2
  }
}
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
type obj2 = DeepPartial<obj>
/**
 * type obj2 = {
    a?: 1 | undefined;
    b?: DeepPartial<{
        c: 2;
    }> | undefined;
  }
*/

Required

// 基础用法
type obj = {
  a: 1,
  b: {
    c: 2
  }
}
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}
type obj2 = Required<MyPartial<obj>>
/**
 *type obj2 = {
    a: 1;
    b: {
        c: 2;
    };
}
*/

Required就是把可选的变成必传的,非常简单,只需要把?去掉

// 基础用法
type obj = {
  a: 1,
  b: {
    c: 2
  }
}
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}
type MyRequired<T> = {
  [K in keyof T]-?: T[K]
}
type obj2 = MyRequired<MyPartial<obj>>
/**
 *type obj2 = {
    a: 1;
    b: {
        c: 2;
    };
}
*/

直接-?就可以了,神奇吧,那如果对象嵌套了,我想你会举一反三了吧,没错还是它递归

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

Readonly

type obj = {
  a: 1,
  b: {
    c: 2
  }
}
type obj2 = Readonly<obj>
/**
 * type obj2 = {
    readonly a: 1;
    readonly b: {
        c: 2;
    };
}
 */

Readonly就是把对象里面的值变成只读的,看这个obj2的类型,我想你知道怎么做了吧

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

Record

type obj = Record<string, any>
/**
 * type obj = {
    [x: string]: any;
}
 */

其实根据上面学的,你已经会实现它了

type MyRecord<K extends keyof any, T> = {
  [P in K]: T
}

type obj = MyRecord<string, any>
/**
 * type obj = {
    [x: string]: any;
}
 */

K extends keyof any: 这里使用了泛型约束,确保 K 是任何类型的键集合的子集。keyof any 表示任何类型的所有键的联合。[P in K]: T: 这是一个映射类型。它表示对于 K 中的每个键 P,都创建一个属性,其值类型是 T

Pick

type MyPick<T extends object, K extends keyof T> = {
  [P in K]: T[K]
}

type obj = MyPick<{a: 1, b: 2}, 'a'>
/***
 * type obj = {
    a: 1;
}
 */

Omit

type MyOmit<T extends object, K extends keyof T> =
 Pick<T, Exclude<keyof T, K>>

type obj = MyOmit<{ a: 1, b: 2 }, 'a'>
/***
 * type obj = {
    b: 2;
}
 */
  • Exclude<keyof T, K>: 使用 keyof T 获取对象 T 的所有键,然后使用 Exclude 排除其中与 K 相同的键。这样得到的是 T 中除去 K 对应键的键集合。
  • Pick<T, ...>: 使用 Pick 从对象 T 中选择具有特定键集合的属性。在这里,我们选择了 T 中除去 K 对应键的其他所有属性。

我们来看看Exclude的实现

Exclude

type MyExclude<T, U> = T extends U ? never : T
type T0 = MyExclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c"

如果T中存在U就剔除(never)否则保留

Extract

很明显就是Exclude的反向操作

type MyExtract<T, U> = T extends U ? T : never
type T0 = MyExtract<"a" | "b" | "c", "a">;
// type T0 = "a"

NonNullable

type T0 = NonNullable<string | number | undefined>;
type T1 = NonNullable<string[] | null | undefined>;
type NonNullable<T> = T & {};

T0 的类型是 string | number。这是因为 NonNullable 类型别名被定义为 T & {},这意味着它接受一个类型 T,并返回一个新的类型,该新类型是 T 和空对象类型的交叉类型。由于 TypeScript 中的交叉类型(T & {})会过滤掉 nullundefined,因此 T0 的类型就是排除了 undefinedstring | number

也可以这样实现

type MyNonNullable<T> = T extends null | undefined ? never : T;

ConstructorParameters

type MyConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;
class C {
  constructor(a: number, b: string) {}
}
type T3 = MyConstructorParameters<typeof C>;
// type T3 = [a: number, b: string]

还是老套路infer推断,这个和我们上面那个获取函数参数类型差不多的,换了个形式。

  • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。
  • T extends abstract new (...args: infer P) => any ? P : never: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的参数类型 P,否则返回 never

InstanceType

class C {
  x = 0;
  y = 0;
}
type MyInstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : never;
type T0 = MyInstanceType<typeof C>;
// type T0 = C

和我们上面那个实现的获取函数类型返回值类型如出一辙和获取类的构造器类型是配对的。

  • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。
  • T extends abstract new (...args: any) => infer R ? R : any: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的实例类型 R,否则返回 never

ThisParameterType

function toHex(this: Number) {
  return this.toString(16);
}
function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

像这种提取类型的都用infer,先猜一猜,内部肯定是判断T是不是一个函数, 第一个参数this infer R是就返回R不是就返回never。 我们看看答案

type ThisParameterType<T> =
  T extends (this: infer U, ...args: never) => any
    ? U
    : unknown;

和我们猜想的差不多,我想你现在应该可以类型编程了吧。

TS内部还有四个内置类型是通过JS来实现的,我们就不研究了

`Uppercase<StringType>`
`Lowercase<StringType>`
`Capitalize<StringType>`
`Uncapitalize<StringType>`

祝大家TS的编程技术越来越好吧,本人非常菜,大佬轻喷。