gvergnaud / hotscript

A library of composable functions for the type-level! Transform your TypeScript types in any way you want using functions you already know.
3.38k stars 57 forks source link
type-level-programming typescript

Higher-Order TypeScript (HOTScript)

A library of composable functions for the type level!

Transform your TypeScript types in any way you want using functions you already know.

image

Features

🚧 work in progress 🚧

Installation

You can find HotScript on npm:

npm install -D hotscript

HotScript is a work-in-progress library, so expect breaking changes in its API.

Examples

Transforming a list

Run this as a TypeScript Playground

import { Pipe, Tuples, Strings, Numbers } from "hotscript";

type res1 = Pipe<
  //  ^? 62
  [1, 2, 3, 4],
  [
    Tuples.Map<Numbers.Add<3>>,       // [4, 5, 6, 7]
    Tuples.Join<".">,                 // "4.5.6.7"
    Strings.Split<".">,               // ["4", "5", "6", "7"]
    Tuples.Map<Strings.Prepend<"1">>, // ["14", "15", "16", "17"]
    Tuples.Map<Strings.ToNumber>,     // [14, 15, 16, 17]
    Tuples.Sum                        // 62
  ]
>;

Defining a first-class function

Run this as a TypeScript Playground

import { Call, Fn, Tuples } from "hotscript";

// This is a type-level "lambda"!
interface Duplicate extends Fn {
  return: [this["arg0"], this["arg0"]];
}

type result1 = Call<Tuples.Map<Duplicate>, [1, 2, 3, 4]>;
//     ^? [[1, 1], [2, 2], [3, 3], [4, 4]]

type result2 = Call<Tuples.FlatMap<Duplicate>, [1, 2, 3, 4]>;
//     ^? [1, 1, 2, 2, 3, 3, 4, 4]

Transforming an object type

Run this as a TypeScript Playground

import { Pipe, Objects, Booleans } from "hotscript";

// Let's compose some functions to transform an object type:
type ToAPIPayload<T> = Pipe<
  T,
  [
    Objects.OmitBy<Booleans.Equals<symbol>>,
    Objects.Assign<{ metadata: { newUser: true } }>,
    Objects.SnakeCaseDeep,
    Objects.Assign<{ id: string }>
  ]
>;
type T = ToAPIPayload<{
  id: symbol;
  firstName: string;
  lastName: string;
}>;
// Returns:
type T = {
  id: string;
  metadata: { new_user: true };
  first_name: string;
  last_name: string;
};

Parsing a route path

Run this as a TypeScript Playground

https://user-images.githubusercontent.com/2315749/222081717-96217cd2-ac89-4e06-a942-17fbda717cd2.mp4

import { Pipe, Objects, Strings, ComposeLeft, Tuples, Match } from "hotscript";

type res5 = Pipe<
  //    ^? { id: string, index: number }
  "/users/<id:string>/posts/<index:number>",
  [
    Strings.Split<"/">,
    Tuples.Filter<Strings.StartsWith<"<">>,
    Tuples.Map<ComposeLeft<[Strings.Trim<"<" | ">">, Strings.Split<":">]>>,
    Tuples.ToUnion,
    Objects.FromEntries,
    Objects.MapValues<
      Match<[Match.With<"string", string>, Match.With<"number", number>]>
    >
  ]
>;

Make querySelector typesafe

Run this as a TypeScript Playground

import * as H from 'hotscript'

declare function querySelector<T extends string>(selector: T): ElementFromSelector<T> | null

interface Trim extends H.Fn {
    return:
    this["arg0"] extends `${infer Prev} ,${infer Next}` ?
    H.$<Trim, `${Prev},${Next}`> :
    this["arg0"] extends `${infer Prev}, ${infer Next}` ?
    H.$<Trim, `${Prev},${Next}`> :
    this["arg0"] extends `${infer Prev}:is(${infer El})${infer Rest}` ?
    H.$<Trim, `${Prev}${El}${Rest}`> :
    this["arg0"] extends `${infer Prev}:where(${infer El})${infer Rest}` ?
    H.$<Trim, `${Prev}${El}${Rest}`> :
    this["arg0"] extends `${infer El}(${string})${infer Rest}` ?
    H.$<Trim, `${El}${Rest}`> :
    this["arg0"] extends `${infer El}[${string}]${infer Rest}` ?
    H.$<Trim, `${El}${Rest}`> :
    this["arg0"]
}

type ElementFromSelector<T> = H.Pipe<T, [
    Trim,
    H.Strings.Split<' '>,
    H.Tuples.Last,
    H.Strings.Split<','>,
    H.Tuples.ToUnion,
    H.Strings.Split<":" | "[" | "." | "#">,
    H.Tuples.At<0>,
    H.Match<[
        H.Match.With<keyof HTMLElementTagNameMap, H.Objects.Get<H._, HTMLElementTagNameMap>>,
        H.Match.With<any, HTMLElement>
    ]>
]>

image

API