Skip to content

Instantly share code, notes, and snippets.

@hmil
Created September 20, 2018 16:34
Show Gist options
  • Save hmil/3aaaf50c6e737eb74b53aa7fc6642ce9 to your computer and use it in GitHub Desktop.
Save hmil/3aaaf50c6e737eb74b53aa7fc6642ce9 to your computer and use it in GitHub Desktop.
Well-typed variable-length tuples in TypeScript
/**
* Arbitrary-length tuples
* =======================
*
* Working with tuples of unknown length in TypeScript is a pain. Most library authors fall back on enumerating all possible
* tuple lengths up to a threshold (see an example here https://github.com/pelotom/runtypes/blob/fe19290d375c8818d2c52243ddc2911c8369db37/src/types/tuple.ts )
*
* In this gist, I'm attempting to leverage recursion to provide support for arbitrary length tuples. This has the potential
* to make some kinds of declarative APIs nicer and enhance type inference in some cases.
* This example shows how to take a variable-length tuple as an input, transform each of its types and use the resulting
* tuple somewhere else.
*
* The hack relies on an internal list representation which is familiar to functionnal programming. This list representation could be used
* to perform all sorts of manipulations of the types in the list. Then the list is re-exported as a tuple.
*
*
* Problems:
* - tsc complains about the recursion in the export helper. I could not find a way to shut down this warning. The type
* inference seems to work well despite this error.
* - I do not know if higher-order types are possible. For instance, you would want a type Transform<List, Transformation>
* which allows you to apply Transformation onto each element of the List. But Transformation has to be generic, and I don't think
* you can pass a generic type as a generic type parameter...
*
* Tested with TypeScript 3.0.3
*/
// General purpose
// Returns the tuple of argument types function T expects
type ArgsOf<T> = T extends (...args: infer ARGS) => any ? ARGS : [];
//Extracts the instance type from a constructor
type Inst<T> = T extends { new (...args: any[]): infer I } ? I : T;
// Define the Type List structure
interface Nil {
h: never;
tail: never;
}
interface Elem<Head, Tail extends List<any>> {
h: Head;
tail: Tail;
}
type List<T> = Nil | Elem<T, any>;
// Utilities to convert a tuple to a type list
interface ImportHelper<T> {
h: T extends (t: infer Head, ...rest: any[]) => void ? Head : never;
tail: T extends (t: any, ...rest: infer Tail) => void ? ImportHelper<(...args: Tail) => void> : never;
}
type Tuple2List<T extends any[]> = ImportHelper<(...args: T) => void>
// Utilities to convert a list back to a tuple
interface List2TupleHelper<L> {
_: L extends Elem<infer Head, infer Tail> ? (h: Head, ...tail: ArgsOf<List2TupleHelper<Tail>['_']>) => void : null; // TypeScript 3.0.3 reports an error here even though it still manages to infer the correct types
}
type List2Tuple<T> = ArgsOf<List2TupleHelper<T>['_']>;
// Usage example - author:
interface AllInsts<T extends List<any>> {
h: T extends Elem<infer Head, any> ? Inst<Head> : never;
tail: T extends Elem<any, infer Tail> ? AllInsts<Tail> : never;
}
type InstsInTuple<T extends any[]> = List2Tuple<AllInsts<Tuple2List<T>>>;
// This function expects a list of constructors and then a function which takes insances from these constructors as arguments.
const factory = <T extends any[]>(...args: T) => (fn: (...args: InstsInTuple<T>) => void) => 'done';
// Usage example - user:
class Monster {
sound: 'rhaa';
eats: 'children';
}
class Horse {
eats: 'grass';
jumps: true;
}
class Person {
rides: Horse;
}
type Instances = InstsInTuple<[Monster, Horse, Person]>;
factory(Monster, Person, Horse)(
// The following instance types are inferred correctly!
(monster, person, horse) => {
const m: Monster = monster; // OK
const p: Person = person; // OK
const h: Horse = horse; // OK
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment