HotScript: The Joy of TypeScript Type Gymnastics

  1. Yesterday, I learned about a library (hotscript) that is very comfortable for ts type programming. Most type gymnastics are not often used, but when needed, a certain type of gymnastics foundation is required.
  2. Let’s first take a look at how to use it and how to call it comfortable.
  3. hotscript link

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

It's easy to understand, but the example is not cool? , come a

1684233474080.jpg

Hotscript provides most of the types of functions that are needed in daily life, and the type conversion operation is now a handy one.

  1. I am sure the content of the article will not be so simple. Next, let’s explore how hotscript is implemented, and I also implemented it briefly. Online experience link

  2. First of all, let's consider the difficulties, then how to control parameters, perform conversions, and how to save types.

    1. If the type is saved, use the interface to realize it, so why use the interface to look at the code

1684233982026.jpgIt is possible to obtain the values ​​of other attributes in the interface, then this is easy to do, and the data can be merged into the interface when the corresponding function is executed in the future.


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

In this way, we can save the required parameters in the interface, and then implement an add and then convert it to a string and can operate in such a combination.

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"]}`;
  }
}

Here is our specific implementation logic. At present, PartialApply and UtilAdd have not been introduced here. UtilAdd is the purest and simplest addition implementation.

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;

The point is PartialApply, but the following may make your scalp tingle, but we can first look at what MergeArgs in PartialApply does, and then look at the PartialApply implementation as a whole. I don’t know if you have found that the generic accepting parameters in PartialApply can be used Number.Addfor a slottype. The main reason here is that the type parameters need to be reorganized, and the default parameters can be combined with the meaning of a parameter position slot.

WeChatc82e1ef1ddf638d105de876afc418523.png

What's the use of this, because it can be used in a way similar to currying, and at the same time, it can avoid the depth of the type being too deep when the generic type is passed in using a similar closure to implement the type recursively. Next, let's look at this Type implementation, of course, this is a little more concise, not in the hotscript source code.

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>>;
}

So now you can implement a type that calls this function

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;

Then we use it

image.png

Maybe the introduction is not easy to understand, but this itself is for the convenience of type operation, allowing users to operate types declaratively, without paying special attention to the key points. Finally, I attach a demo of my implementation, the specific api and hotScript There is a little discrepancy.

image.png

Guess you like

Origin juejin.im/post/7233695223402381369