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

try new core idea #104

Open gvergnaud opened 11 months ago

gvergnaud commented 11 months ago

Here is a POC for a slightly different take on hkts. Here are the main ideas

Here is what using it feels like:

import { $, Arg0, Arg1, Args, Fn } from 'hot2';

type ExpectNumber<a extends number> = [a];
// arguments are typed internally:
interface TakeNumAndStr extends Fn<[number, string], boolean> {
  works: ExpectNumber<Arg0<this>>; // ✅
  fails: ExpectNumber<Arg1<this>>;
  //                  ~~~~~~~~~~  ❌
  return: true;
}

interface Div extends Fn<[number, number], number> {
  return: NumberImpl.Div<Arg0<this>, Arg1<this>>;
}

/**
 * Full application
 */

type x = $<Div, 10, 2>; // 5
//   ^?
type y = $<Div, "10", 2>;
//              ~~~ ❌
type z = $<Div, 11, "2">;
//                  ~~~ ❌

/**
 * Partial application in order
 */

type Div1 = $<Div, 10>;
type Three = $<Div1, 2>;
//    ^?
type w = $<$<Div, 10>, "2">;
//                     ~~~ ❌

/**
 * Partial application different order
 */
type DivBy2 = $<Div, _, 2>;
//   ^?
type q = $<DivBy2, 10>; // 5 ✅
//   ^?
type r = $<$<Div, _>, 10, 5>; // ✅
//   ^?

type TakeStr = $<TakeNumAndStr, 10>;
//   ^? Ap<TakeNumAndStr, [10]>
type e = $<TakeStr, 10>;
//                  ~~ ❌

type TakeNum = $<TakeNumAndStr, _, "10">;
//   ^?Ap<TakeNumAndStr, [_, "10"]>
type s = $<TakeNum, 10>;
//                  ~~ FIXME

/**
 * Higher order
 */

interface Map extends Fn<[Fn, any[]]> {
  return: Args<this> extends [infer fn extends Fn, infer tuple]
    ? { [key in keyof tuple]: $<fn, tuple[key]> }
    : never;
}

type z2 = $<Map, $<Div, _, 2>, [2, 4, 6, 8, 10]>;
//   ^? [1, 2, 3, 4, 5]

interface Add extends Fn<[number, number], number> {
  return: NumberImpl.Add<Arg0<this>, Arg1<this>>;
}

interface Mul extends Fn<[number, number], number> {
  return: NumberImpl.Mul<Arg0<this>, Arg1<this>>;
}

type ReduceImpl<fn extends Fn, acc, xs> = xs extends [
  infer first,
  ...infer rest
]
  ? ReduceImpl<fn, $<fn, acc, first>, rest>
  : acc;

interface Reduce<A = any, B = any> extends Fn<[Fn<[B, A], B>, B, A[]], B> {
  return: Args<this> extends [infer fn extends Fn, infer acc, infer tuple]
    ? ReduceImpl<fn, acc, tuple>
    : never;
}

type reduced1 = $<Reduce<number, number>, Add, 0, [2, 4, 6, 8, 10]>;
//   ^? 30
type reduced2 = $<Reduce<number, number>, Mul, 1, [2, 4, 6, 8, 10]>;
//   ^? 3840
type reduced3 = $<Reduce<number, number>, Mul, 1, ["2", "4", "6", "8", "10"]>;
//                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~ ❌
type reducedOops = $<Reduce, Mul, 1, "oops">;
//                                   ~~~~~~ ❌

interface NumToStringReducer extends Fn<[string, number], string> {
  return: `${Arg0<this>}${Arg1<this>}`;
}

interface StringToNumReducer extends Fn<[number, string], number> {
  return: NumberImpl.Add<Arg0<this>, StringImpl.Length<Arg1<this>>>;
}

// prettier-ignore
type reduced4 = $<Reduce<string, number>, StringToNumReducer, 1, ["a", "aa", "aaa", "aaaa", "aaaaa"]>;
//     ^? 16

// prettier-ignore
type reduced5 = $<Reduce<string, number>, NumToStringReducer, 1, ["a", "aa", "aaa", "aaaa", "aaaaa"]>;
//                                        ~~~~~~~~~~~~~~~~~~ ❌

interface ToString extends Fn<[number], string> {
  return: `${Arg0<this>}`;
}

interface ToNumber extends Fn<[string], number> {
  return: Arg0<this> extends `${infer N extends number}` ? N : never;
}

interface Prepend extends Fn<[string, string], string> {
  return: this["args"] extends [
    infer first extends string,
    infer str extends string
  ]
    ? `${first}${str}`
    : never;
}

type Times10<T extends number> = $<ToNumber, $<Prepend, "1", $<ToString, T>>>;

type WrongComposition1<T extends string> = $<Prepend, "1", $<ToNumber, T>>;
//                                                         ~~~~~~~~~~~~~~ ❌
type WrongComposition2<T extends number> = $<Add, 1, $<ToString, T>>;
//                                                   ~~~~~~~~~~~~~~ ❌

type test1 = Times10<10>;
//    ^? 110
geoffreytools commented 11 months ago

I notice that your interface and functionality is becoming more and more similar to free-types although the underlying implementation is very different. I am considering abandoning this project considering how well you are doing. I talk about a few challenges of implementing type constraints in length in my guide if you want to skim through it.

I wonder if we hit the same design limitations. How does this behave regarding variance in higher order scenarios for example? I also found conditional types to make TS detect excessively deep type instantiations in some scenarios where they are used to defuse type constraints, making it harder to compose types, which is why I default to intersection and provide different helpers for different situations.

folks at https://github.com/Effect-TS might be interested by an implementation enabling specifying variance. At current they use positional arguments with a specific variance for each, which is appropriate for their needs but makes the type lambdas awkward to use and read for the few power users that would want to define theirs. If the community could settle on one interface that is type safe and general that would be awesome.