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

前言

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

一道开胃菜

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是怎么实现的:

ts 复制代码
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

ts 复制代码
// 基础用法
type promise = Promise<string>
type p = Awaited<promise> // string
ts 复制代码
// 定义一个返回 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是怎么实现的

ts 复制代码
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

这里有个思路

ts 复制代码
type MyAwait<T> = T extends Promise<infer P> ? P : never
type p = MyAwait<Promise<string>> // string

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

ts 复制代码
type MyAwait<T> = T extends Promise<infer P> ? P : never
type p = MyAwait<Promise<Promise<string>>> // Promise<string>

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

ts 复制代码
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内部是如何实现的

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

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

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

ts 复制代码
// 基础用法
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键的值),反复想想是不是这回事。

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

ts 复制代码
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

ts 复制代码
// 基础用法
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就是把可选的变成必传的,非常简单,只需要把?去掉

ts 复制代码
// 基础用法
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;
    };
}
*/

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

ts 复制代码
type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K]
}

Readonly

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

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

ts 复制代码
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}
ts 复制代码
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}

Record

ts 复制代码
type obj = Record<string, any>
/**
 * type obj = {
    [x: string]: any;
}
 */

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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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的反向操作

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

NonNullable

ts 复制代码
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

也可以这样实现

ts 复制代码
type MyNonNullable<T> = T extends null | undefined ? never : T;

ConstructorParameters

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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。 我们看看答案

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

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

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

ts 复制代码
`Uppercase<StringType>`
`Lowercase<StringType>`
`Capitalize<StringType>`
`Uncapitalize<StringType>`

可以看看我的这篇文章vue里面对于TS的使用 # 突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?

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

相关推荐
Embrace9 分钟前
NextAuth实现Google登录报错问题
前端
小海编码日记11 分钟前
Geadle,Gradle插件,Android Studio and sdk版本对应关系
前端
粤M温同学15 分钟前
Web前端基础之HTML
前端·html
love530love21 分钟前
是否需要预先安装 CUDA Toolkit?——按使用场景分级推荐及进阶说明
linux·运维·前端·人工智能·windows·后端·nlp
泯泷1 小时前
「译」为 Rust 及所有语言优化 WebAssembly
前端·后端·rust
LinXunFeng1 小时前
Flutter - GetX Helper 如何应用于旧页面
前端·flutter·开源
紫薯馍馍2 小时前
Dify创建 echarts图表 (二)dify+python后端flask实现
前端·flask·echarts·dify
梦想很大很大2 小时前
把业务逻辑写进数据库中:老办法的新思路(以 PostgreSQL 为例)
前端·后端·架构
李三岁_foucsli2 小时前
从生成器和协程的角度详解async和await,图文解析
前端·javascript
柚子8162 小时前
CSS自定义函数也来了
前端·css