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

Not work when Call argument is generic parameter #107

Closed cstria0106 closed 11 months ago

cstria0106 commented 11 months ago

Code:

import { Call, Fn } from 'hotscript';

interface Just extends Fn {
  return: this['arg0'];
}

function fn<T extends string>(a: T, b: Call<Just, T>) {
  type JustString = Call<Just, String>;

  const aLength = a.length;
  const bLength = b.length;

  const js: JustString = '';
  const jsLength = js.length;
}

Error:

Property 'length' does not exist on type '(Equal<T, unique symbol> extends true ? [] : [T]) extends [infer arg, ...any[]] ? arg : never'.

11   const bLength = b.length;
                       ~~~~~~
ecyrbe commented 11 months ago

For uses in generics, you can use Apply :

interface Just extends Fn {
  return: this['arg0'];
}

function fn<T extends string>(a: T, b: Apply<Just, [T]>) {
  type JustString = Call<Just, string>;

  const aLength = a.length;
  const bLength = b.length;

  const js: JustString = '';
  const jsLength = js.length;
}
cstria0106 commented 11 months ago

Thank you! Can you tell me what makes Call and Apply different? Looking at the comments, it looks like Call and Apply have similar behavior

Call:

Calls a HOTScript function.

Apply:

Call a HOTScript function with the given arguments.
ecyrbe commented 11 months ago

Call does remove placeholder parameters (they are implicit), while Apply does not. examples from the docs :

type T0 = Call<Numbers.Add<1, 2>>; // 3
type T1 = Call<Numbers.Add<1>, 2>; // 3
type T2 = Call<Numbers.Add, 1, 2>; // 3
type T3 = Call<
   Tuples.Map<Strings.Split<".">, ["a.b", "b.c"]>
>; // [["a", "b"], ["b", "c"]]
cstria0106 commented 11 months ago

Does this mean that generics cannot be used with Call to support the calling semantics of Call?

ecyrbe commented 11 months ago

No you can use Call in generics, just that since the results of HOTScript are complex type computations, they can only be evaluated at the instanciation of your generics. Your example here is a simple one, so there is a path to resolve it before instanciation, but when doing transformations on complex types (example, records), you have no other way in TS than cast. you'll see a lot of TS generics heavy libraries doing as any cast to silence those TS errors. But from a user point of view, using your library, there is no issue. So here an alternate solution (not pretty) :

function fn<T extends string>(a: T, b: Call<Just, T>) {
  type JustString = Call<Just, string>;

  const aLength = a.length;
  const bLength = (b as unknown as string).length;

  const js: JustString = '';
  const jsLength = js.length;
}

the user of the function passing parameters will have Call be resolved just fine

ecyrbe commented 11 months ago

here a concrete example of what i'm saying in tanstack router : https://github.com/TanStack/router/blob/8814f47ba42a2db5ba6a909e54ccdbd4138317b9/packages/router/src/route.ts#L626

and tanstack router is not using HOTScript, but they face the same issue, aka RouterOptions can only be resolved by the user calling the function.

ecyrbe commented 11 months ago

This situation could be fixed if we somehow could tell typescript that the result satisfies some shape. but satisfies keyword does not work on generics utility types at the moment

cstria0106 commented 11 months ago

Really thank you for your response. My issue and questions have been resolved and I will close this issue.