Skip to content

Instantly share code, notes, and snippets.

@hmil
Created September 20, 2018 16:34
Show Gist options
  • Select an option

  • Save hmil/3aaaf50c6e737eb74b53aa7fc6642ce9 to your computer and use it in GitHub Desktop.

Select an option

Save hmil/3aaaf50c6e737eb74b53aa7fc6642ce9 to your computer and use it in GitHub Desktop.

Revisions

  1. hmil created this gist Sep 20, 2018.
    105 changes: 105 additions & 0 deletions tuples.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    /**
    * 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
    });