Skip to content

Idiomatic HKTs and Variadic Generic Types #45438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
5 tasks done
keyvan-m-sadeghi opened this issue Aug 13, 2021 · 2 comments
Open
5 tasks done

Idiomatic HKTs and Variadic Generic Types #45438

keyvan-m-sadeghi opened this issue Aug 13, 2021 · 2 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@keyvan-m-sadeghi
Copy link

keyvan-m-sadeghi commented Aug 13, 2021

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:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Higher Kinded Types

Suggested syntax:

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

Properties

  • Positional clarity, same syntax for definition and invocation, visually similar to HOF calls: f(x)(y)(z)
type WrappedArray<S><T> = S<T>[];
type ArrayOfPromises<T> = WrappedArray<Promise><T>;
// With a glance at the definition, we'll know that Promise is
// positionally assigned to S when invoked
  • Spontaneous logic: more to the left => higher the kind (again similar to HOFs)
  • Omission of lower kind = implicit inclusion
type ArrayOfAsyncIterables = WrappedArray<AsyncIterable>;
// == type ArrayOfAsyncIterables<T> = WrappedArray<AsyncIterable><T>;

Variadic Generic Types

Suggested syntax:

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

Properties

  • Reuses spread operator, same as in Variadic Tuple Types, exact match with how parameters are spread in JS
  • Paves the way for generic inference
type Generics<F extends (...args) => any> = F extends <...infer G>(...args) => any
  ? G
  : never;

📃 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 RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Aug 13, 2021
@RyanCavanaugh
Copy link
Member

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
Copy link
Author

keyvan-m-sadeghi commented Aug 13, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants