HotScript:玩转 TypeScript 类型体操的乐趣

  1. 昨天了解到一个对于ts类型编程非常舒适的一个库(hotscript),类型体操大多数来说都不会经常使用,但是需要的时候就需要一定的类型体操基础。
  2. 先来看看具体怎么使用,怎么才叫舒适。
  3. hotscript链接

223854503-b54f6a62-9f21-4953-aaa3-5d54699516a7.png

很容易理解,示例还没看爽?,在来一个

1684233474080.jpg

hotscript,提供了日常需要的绝大数类型函数,进行类型转换操作现在属于是手拿把掐。

  1. 肯定文章内容不会这么简单,接下来就来探索一下hotscript是怎么实现的,并我也简单实现了一下,在线体验链接

  2. 首先我们先考虑难点,那么就是如何去控制参数,执行转换,类型如何保存下来。

    1. 类型保存的话,使用interface来实现,那么为什么使用interface来看代码

1684233982026.jpg 是可以把在interface中获取其他属性的值,那么这就好做了,完全可以在未来执行到对应函数的时候把数据合并到接口中。


interface Fn {
  [rawArgs]: unknown;
  arg: this[rawArgs] extends Array<unknown> ? this[rawArgs] : unknown[];
  arg0: this[rawArgs] extends [infer arg, ...any] ? arg : never;
  arg1: this[rawArgs] extends [any, infer arg, ...any] ? arg : never;
  running: unknown;
}
type foo = Print<Fn & { [rawArgs]: [1, 2, 3, 4] }>;

type f1  = foo["arg"]//[1, 2, 3, 4]
type f2  = foo["arg0"]//1
type f3  = foo["arg1"]//2

这样我们就可以在接口中保存所需要的参数,接下来实现一个的add之后转string并且可以这样的组合操作。

namespace Number {
   export type Add<
      A extends number | slot = slot,
      B extends number | slot = slot
    > = PartialApply<AddFn, [A, B]>;

  interface AddFn extends Fn {
    running: this["arg"] extends [
      infer A extends number,
      infer B extends number
    ]
      ? UtilAdd<A, B>
      : never;
  }
  
 //在来一个简单的toString来实现接下里的组合使用
 namespace String {
  export type toString = PartialApply<toStringFn, []>;
  interface toStringFn extends Fn {
    running: `${this["arg0"]}`;
  }
}

这里就是我们具体的实现逻辑,当前这里还没有介绍PartialApply和UtilAdd, UtilAdd就是纯粹的最简单的加法实现。

type Build<
  T extends number,
  Result extends Array<never> = [],
> = Result["length"] extends T ? Result : Build<T, [...Result, never]>;

export type Add<A extends number, B extends number> = [
  ...Build<A>,
  ...Build<B>
]["length"];

type test = Add<1,2> // 3;

重点是PartialApply不过接下里也许会让你头皮发麻,不过我们可以先看下PartialApply中的MergeArgs做了什么,然后整体看PartialApply实现, 不知道你有没有发现Number.Add中的泛型接受参数是可以为一个slot类型。 这里主要是需要对类型参数重新组和,可以合并默认参数,和一个参数位置插槽的的意思。

WeChatc82e1ef1ddf638d105de876afc418523.png

这样有什么用呢,因为可以实现类似柯里化的使用方式,同时也可以避免因为泛型的传入使用类似闭包实现在递归执行类型的时候,导致深度过深,接下来就看下这个类型实现,当然这个是稍微简练过一点的,并不是hotscript源码中的。

type MergeArgs<
  pipedArgs extends any[],
  partialArgs extends any[],
  Result extends unknown[] = []
> = partialArgs extends [infer partialFirst, ...infer lastPartialFirst]
  ? partialFirst extends slot
    ? pipedArgs extends [infer pipedArgsFirst, ...infer lastPipedArgsFirst]
      ? MergeArgs<
          lastPipedArgsFirst,
          lastPartialFirst,
          [...Result, pipedArgsFirst]
        >
      : [...Result, ...lastPartialFirst]
    : MergeArgs<pipedArgs, lastPartialFirst, [...Result, partialFirst]>
  : [...Result, ...pipedArgs];

interface PartialApply<T extends Fn, partialArgs extends unknown[]> extends Fn {
  running: Running<T, MergeArgs<this["arg"], partialArgs>>;
}

那么现在就可以去实现一个调用这个函数的类型

type Running<G extends Fn, P> = (G & {
    //在这里我们对参数进行合并传入
  [rawArgs]: P extends Array<any> ? P : [P];
})["running"];

export type Pipe<T, G extends Fn[]> = G extends [
  infer A extends Fn,
  ...infer B extends Fn[]
]
  ? Pipe<Running<A, T>, B>
  : T;

然后我们来使用它

image.png

也许介绍的不太容易理解,不过这个本身就为了方便类型操作,让使用者面向声明式去操作类型,不用关注特别多重点,最后在在附上一张我的实现的demo,具体api和hotScript有一点出入。

image.png

猜你喜欢

转载自juejin.im/post/7233695223402381369