microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.1k stars 12.37k forks source link

Idiomatic HKTs and Variadic Generic Types #45438

Open keyvan-m-sadeghi opened 3 years ago

keyvan-m-sadeghi commented 3 years ago

Suggestion

Pertaining to the discussion in #1213 and having looked at similar suggestions (borrowing ideas where appropriate), this is a strawman proposal (syntax only) on HKTs and Variadic Generic Types that adheres to existing syntax and mindset in JS/TS , hence named "idiomatic".

🔍 Search Terms

HKT Higher Order Types Higher Kinded Types Higher Order Type Functions Higher Order Function Generic Variadic Generic Types

✅ Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

Higher Kinded Types

Suggested syntax:

type HKT<S><T><V> = S<T<V>>;

Properties

Variadic Generic Types

Suggested syntax:

function aFunction<...T>(...args) {
  return anotherFunction<...T>(...args);  
}

Properties

📃 Motivating Example

I was looking at the code in the streaming-iterables package, the pattern of defining an _fn and then overloading it with an fn including a curried version seems a case of DRY, happens in all functions the package exposes.

Below code was my attempt at a general currying overload implementation for the package:

type Leading<T extends any[]> = T extends [...infer I, infer _] ? I : never;
type Last<T extends any[]> = T extends [...infer _, infer I] ? I : never;

function curried<F extends (...args) => any>(
  fn: F
): {
  (...args: Parameters<F>): ReturnType<F>;
  (...args: Leading<Parameters<F>>): (curried: Last<Parameters<F>>) => ReturnType<F>;
} {
  return (...args) =>
    args.length == fn.length - 1
      ? curried => fn(...[...args, curried]) 
      : fn(...args);
}

This works well until generics come into play:

function a<T>(b: string, c: number, d: T[]) {
  return [b, c, ...d];
}

const f = curried(a);

f('hi', 42, [1, 2, 3]) // d: unknown[], expected: number[]
f('hi', 42) // curried: unknown[], expected: T[]

SO Question

💻 Use Cases

Using this proposal, the example described in the previous section can be covered:

function curried<F extends (...args) => any>(
  fn: F
): {
  <...T extends Generics<F>>(...args: Parameters<F><...T>): ReturnType<F><...T>;
  <...T extends Generics<F>>(...args: Leading<Parameters<F><...T>>):
    (curried: Last<Parameters<F><...T>>) => ReturnType<F><...T>;
} {
  return // same as before
}
RyanCavanaugh commented 3 years ago

This is a good start to describe some uncontroversial syntax, but doesn't go far enough to describe behavior outside of the straightforward cases, which is where 99% of the difficulty is here. It seems like this belongs more as a comment at #1213?

keyvan-m-sadeghi commented 3 years ago

Agreed @RyanCavanaugh. Main goal was to demo it's probably possible to introduce HKTs and Variadic Generic Types without imposing more cognitive overhead (syntaxes like <-> or ^2). I changed the intro to clarify that it's only strawman at this stage, but the issue can serve as a reference when discussing new syntaxes. Already commented in #1213.