Typescript: Mapped tuple types

Created on 26 Jul 2018  ·  29Comments  ·  Source: microsoft/TypeScript

Search Terms

generic mapped tuple types overrides

Suggestion

The syntax here is up for debate, but the basic idea is to be able to apply the idea behind mapped object types to tuples. I would suggest the following:

type MappedTuple<T extends any[]> = [F<E> for E in T];

This would apply a type mapping to each of the element types of the tuple.

For example:

type PromiseTuple<T extends any[]> = [Promise<E> for E in T];
type T1 = Promise<[number, string, boolean]>;
// [Promise<number>, Promise<string>, Promise<boolean>]

Use Cases

This would make it much easier to write functions that take a variable number of arguments that all have to conform to some form and be able to reference the underlying type. This eliminates the need to write a ton of overrides:

all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]>;
all<T1, T2>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;
all<T1, T2, T3>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
all<T1, T2, T3, T4>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>]): Promise<[T1, T2, T3, T4]>;
...
all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;

Examples

The basic usage of this would be as follows:

type TypeMap<T> = { wrap: T };
type MapTuple<T extends any[]> = [TypeMap<E> for E in T];
type T2 = MapTuple<[number, string, ...boolean[]]>
// [{ wrap: number }, { wrap: string }, ...{ wrap: boolean }[]];

There are many compelling examples of how this would simplify library code. For example, above, instead of having 10 overrides for Promise.all, we could define it simply as:

type PromiseLikeTuple<T extends any[]> = [E | PromiseLike<E> for E in T];
all<T extends any[]>(...values: PromiseLikeTuple<T>): Promise<T>;

Similarly RxJS' combineLatest would similarly benefit (Note: I've moved the project argument to the head of the list because rest arguments must appear last, this differs from the current definition of combineLatest)

type ObservableTuple<T extends any> = [ObservableInput<E> for E in T];
combineLatest<T extends any[], R>(project: (...values: T) => R, ...observables: ObservableTuple<T>): Observable<R>;

This is especially useful because there are only 6(?) overrides for combineLatest meaning that providing 7 or more observable inputs would mean that your types would no longer be strict.

Currently

I don't believe that it is possible to construct a mapped tuple type currently. This would be made possible with the addition of recursive generic types and made easy to use with the addition of generic generic type arguments:

// Currently possible
type Head<T extends any[]> = T extends [infer U, ...any[]] ? U : never;
type Tail<T extends any[]> = ((...args: T) => void) extends (head: any, ...tail: infer U) => any ? U : never;

// Currently errors
type SpecificMapTuple<T extends any[]> = [F<Head<T>>, ...SpecificMapTuple<Tail<T>>];
type GenericMapTuple<F<~>, T extends any[]> =[F<Head<T>>, ...GenericMapTuple<F, Tail<T>>];

function all<T extends any[]>(...values: GenericMapTuple<PromiseLike, T>): Promise<T>;

Note: I think adding this functionality via recursive generic types and high order generic type arguments is probably a more flexible and elegant solution that will enable even more interesting typings but it would be nice to have both of those and the mapped tuple types above.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. new expression-level syntax)
In Discussion Suggestion

Most helpful comment

Not sure if this still counts as a backdoor recursion, but by using an interface in the reduction step it is possible to recurse without potentially crashing the compiler (at least in my tests):

// Tuple operations Cons, Head, Tail

type Head<T> = T extends [infer U, ...unknown[]] ? U : never

type Tail<T> = T extends Array<any>
  ? ((...args: T) => never) extends ((a: any, ...args: infer R) => never)
    ? R
    : never
  : never

type Cons<T extends any[], H> = ((h: H, ...t: T) => any) extends ((...x: infer X) => any) ? X : never

// Generic lazy tuple reduction

interface Reduction<Base, In> {
  0: Base
  1: In
}

type Reduce<T extends Array<any>, R extends Reduction<any, any>> = R[[T] extends [[]] ? 0 : 1]

// Tuple reversal

interface ReverseRec<H extends Array<any>, T extends Array<any>>
  extends Reduction<H, Reduce<Tail<T>, ReverseRec<Cons<H, Head<T>>, Tail<T>>>> {}

type Reverse<T> = [T] extends [Array<any>] ? Reduce<T, ReverseRec<[], T>> : never

// Currying, finally

interface CurryRec<H, T extends Array<any>>
  extends Reduction<H, Reduce<Tail<T>, CurryRec<(...args: [Head<T>]) => H, Tail<T>>>> {}

type Curry<F extends (...args: any[]) => any> = Reverse<Parameters<F>> extends infer R
  ? R extends any[]
    ? Reduce<R, CurryRec<ReturnType<F>, R>>
    : never
  : never

declare function curry<F extends (...args: any[]) => any>(f: F): Curry<F>
declare const f: (a: number, b: string, c: symbol, d: boolean, e: void) => boolean

const curried = curry(f) // (args_0: number) => (args_0: string) => (args_0: symbol) => (args_0: boolean) => (args_0: void) => boolean

(Playground link)

It seems that even if you specify a recursion interface that would never terminate (example), the compiler does not crash since interfaces are not eagerly evaluated. I also implemented variadic compose using Reduction and Reduce without any visible compilation performance hit.

All 29 comments

FWIW, here's the closest I think we can come today, using my favorite encoding of higher-kinded types (which maybe I should release as a library). We need to write as many cases for MapTuple as we think we'll ever need, but then the boilerplate to apply MapTuple to a given mapping is reasonable.

const INVARIANT_MARKER = Symbol();
type Invariant<T> = {
  [INVARIANT_MARKER](t: T): T
};

interface TypeFuncs<C, X> {}

const FUN_MARKER = Symbol();
type Fun<K extends keyof TypeFuncs<{}, {}>, C> = Invariant<[typeof FUN_MARKER, K, C]>;

const BAD_APP_MARKER = Symbol();
type BadApp<F, X> = Invariant<[typeof BAD_APP_MARKER, F, X]>;
type App<F, X> = [F] extends [Fun<infer K, infer C>] ? TypeFuncs<C, X>[K] : BadApp<F, X>;

const BAD_MAP_TUPLE = Symbol();
type MapTuple<F, T> =
    [T] extends never[] ? never[] :
    [T] extends [[infer T1]] ? [App<F, T1>] :
    [T] extends [[infer T1, infer T2]] ? [App<F, T1>, App<F, T2>] :
    [T] extends [[infer T1, infer T2, infer T3]] ? [App<F, T1>, App<F, T2>, App<F, T3>] :
    [T] extends [[infer T1, infer T2, infer T3, infer T4]] ? [App<F, T1>, App<F, T2>, App<F, T3>, App<F, T4>] :
    [T] extends [[infer T1, infer T2, infer T3, infer T4, infer T5]] ? [App<F, T1>, App<F, T2>, App<F, T3>, App<F, T4>, App<F, T5>] :
    [T] extends [[infer T1, infer T2, infer T3, infer T4, infer T5, infer T6]] ? [App<F, T1>, App<F, T2>, App<F, T3>, App<F, T4>, App<F, T5>, App<F, T6>] :
    [T] extends [[infer T1, infer T2, infer T3, infer T4, infer T5, infer T6, infer T7]] ? [App<F, T1>, App<F, T2>, App<F, T3>, App<F, T4>, App<F, T5>, App<F, T6>, App<F, T7>] :
    [T] extends [[infer T1, infer T2, infer T3, infer T4, infer T5, infer T6, infer T7, infer T8]] ? [App<F, T1>, App<F, T2>, App<F, T3>, App<F, T4>, App<F, T5>, App<F, T6>, App<F, T8>] :
    Invariant<[typeof BAD_MAP_TUPLE, F, T]>;

//type PromiseTuple<T extends any[]> = [Promise<E> for E in T];
const F_Promise = Symbol();
interface TypeFuncs<C, X> { 
    [F_Promise]: Promise<X>;
}
type F_Promise = Fun<typeof F_Promise, never>;
type PromiseTuple<T> = MapTuple<F_Promise, T>;

type T1 = PromiseTuple<[number, string, boolean]>;

Just to give an update, we've recently been discussing this within the team. We know this is one of the next problems we want to tackle.

We've wondered whether or not it makes sense for mapped object types to just specially act on tuple types.

So that

type PartialTuple = Partial<[T1, T2, T3]>

becomes

type PartialTuple = [T1?, T2?, T3?]

Or more broadly, something like

type T = [T1, T2, T3];
type WrappedTuple = { [K in keyof T]: Wrap<T[K]> };

is equivalent to

type T = [T1, T2, T3];
type WrappedTuple = [Wrap<T1>, Wrap<T2>, Wrap<T3>];

This is technically a breaking change, but is likely desirable for most cases.

Now, this is just my opinion, but to be honest, I am much more partial to the syntax that you have proposed. I would much rather not introduce a new special case for tuples on mapped object types.

However, this means that you now need to introduce a new Tuple version of every type alias helper in lib.d.ts (e.g. PartialTuple, RequiredTuple, etc.).

If a "tuple-like" object (sequential "numeric" keys starting from "0", a compatible length property, and a bunch of array methods acting on the union of the element types) were automatically promoted to a tuple, then you could use mapped types to do something like it today:

type StripTuple<T extends any[] & (number extends T['length'] ? never : unknown)> =
  Pick<T, Exclude<keyof T, keyof Array<any>>>

type WrapTuple<T extends any[] & (number extends T['length'] ? never : unknown)> =
  Array<keyof StripTuple<T> extends infer K ? K extends any ? Wrap<T[K]> : never : never> &
  { length: T['length'] } &
  { [K in keyof StripTuple<T>]: Wrap<T[K]>}

type Wrapped = WrapTuple<[string, number, boolean]>
// essentially the same as [Wrap<string>, Wrap<number>, Wrap<boolean>]

But this doesn't really work because it isn't allowed in spread/rest positions... it's not seen as an "array type".


If mapped types do end up being special-cased to behave differently with arrays/tuples, that would probably be fine (actually, I feel like this was one of the motivating use cases for conditional types, since arrays were always messing things up)... as long as there were some way to work around it in the rare instances where you need to map an array/tuple like an object.

Glad to hear that this is being discussed and thanks for the feedback!

In my mind the main benefit of special casing mapped object types (outside of what's obvious/been mentioned i.e. familiarity, no extra type aliases) is that the syntax for adding/dropping type modifiers is pretty reasonable. Compare:

type M1 = { [K in keyof T]-?: T[K] };

To some of these potential (but ugly) options:

type M2 = [E for E-? in T];
type M3 = [E-? for E in T];

That being said I don't think that this should make or break which syntax is used. What's more important in my mind is what special casing mapped object types will do to the definition of keyof. This operator has already changed a decent amount recently and now it would change again when in a certain position for a certain type? This feels confusing and unnecessary to me.

As stated above, if the special case for mapped objects were added, it would still probably still be useful to occasionally treat array as objects for the purposes of mapping. Then we'd need some other special case / syntax for treating tuples as objects for mapping. Which would beg the question, why not just have some different syntax for mapping tuples in the first place?

How about this?

type M1 = [ [K in keyof T]-?: T[K] ];

Binding a variable to the index rather than to the element type of the original tuple lets you do lots of additional things (which may or may not be useful in practice) like zip two tuples:

type Zip<T1, T2> = [ [K in keyof T1 & keyof T2]: [T1[K], T2[K]] ];

The trouble is that we want only the numeric keys of T. We could say that in a mapped tuple, whatever you specify as the constraint type, only the numeric keys are kept, but that might be too tricky. To be consistent with mapped objects, we should probably require the specified constraint type to include only numeric strings. We could define a new built-in type Index that is the infinite union "0" | "1" | ... and then write:

type M1 = [ [K in keyof T & Index]-?: T[K] ];

Or a new operator indexof T that is equivalent to keyof T & Index. Unfortunately keyof T & number gives number, which doesn't work for present purposes, because tuples have a numeric index signature.

@mattmccutchen

I think you can get just the "numeric" keys by doing Exclude<keyof T, keyof Array<any>>, as in StripTuple<>. (I do often want something like your Index type though... I usually call it NumericKey.)

That being said, I think that we adopt the [ [K in keyof T]: ... ] syntax (wrapped in [] instead of {}), it will just be understood that K in keyof T will only end up iterating over the numeric keys of T, and that the array methods and other special properties will inherit from Array<L> where L is the union of the post-mapped element types.

I think you can get just the "numeric" keys by doing Exclude<keyof T, keyof Array<any>>

Only assuming you started with a tuple type without extra properties. We might want to make it possible to do more wild and crazy things so we don't end up getting more feature requests in another few months.

26063 went in - please give it a shot and give us feedback.

Will do, thanks for such a quick turnaround!

@DanielRosenwasser @jscheiny should this issue be closed then?

Yeah, this looks good, will file other follow ups separately.

It would be awesome if we could transform array into object in a similar manner, so we could have typed Object.fromEntries

Just wondering, would this somehow make possible this use case?

Right now I have an "enumeration" function that given an array of strings it has to return a type which is the union of all passed string literal, for which right now I'm doing:

type UnionStringArray<T extends string[]> = T[0] | T[1] | T[2] | ...
function enumeration<T extends string[]>(values: T): UnionStringArray<T> { ... }

// type of enumeration(["a", "b"]) === "a" | "b"

The problem I have with that is that approach is that if I want to support up to 30 arguments then I have to write T[0] | .... | T[29]

Would this new feature help in this case?

@xaviergonz Can't you just use type UnionStringArray<T extends string[]> = T[number]?

@mattmccutchen apparently yes and no

type UnionStringArray<T extends string[]> = T[number]
function enumeration<T extends string[]>(options: T): UnionStringArray<T>

type TA = UnionStringArray<["a", "b"]> // this gives as type "a" | "b", good!
const a = enumeration(["a", "b"]) // but for some reason the type of this one is string :-/

that's with latest TS 3.1.0-dev, TS 3.0.1 and TS 2.9.2

@xaviergonz That's due to widening of the argument to enumeration and has nothing to do with the definition of UnionStringArray. To stop the widening, try this:

declare function enumeration<T extends string>(options: T[]): UnionStringArray<T[]>;

@mattmccutchen that seems to work much better! thanks!

is there a way to also make it work when the array is declared independently? this is

enumeration(["a", "b"]) // type is "a" | "b", good!
const arr = ["a", "b"]
enumeration(arr) // type is string :-/

right now the only solution I found for the latest is to have a million overloads like

enumeration<A extends string>(options: [A]): A
enumeration<A extends string, B extends string>(options: [A, B]): A | B

@xaviergonz No, once the array is assigned to a variable, its type is string[]. Even overloads for enumeration don't seem to help; see the playground.

@mattmccutchen ah I see, still much better than any other solution I had before, thanks for the help!

@DanielRosenwasser WrappedTuple would be quite useful for arguments and rest parameters:

const lift = <A extends any[], R>(fn: (...args: A) => R) => (
  ...args: { [K in keyof A]: Future<T[K]> }
) => Future.collect(args).map(arr => fn.apply(undefined, arr) as R)

currently has to be loosely typed like

const lift = <A extends any[], R>(fn: (...args: A) => R) => (
  ...args: Future<A[number]>[]
) => Future.collect(args).map(arr => fn.apply(undefined, arr) as R)

We'd definitely like this in our own library. We have something similar to Promise.all, and having to expose and understand all the overloads here isn't pleasant :)

this functionality has helped a lot to work with something similar to variadic kinds, check this (https://github.com/Microsoft/TypeScript/pull/26063):

type Tail<F extends Function, S extends Number> =
  S extends 0 ? (F extends (...args: infer TArgs) => any ? TArgs : never) :
  S extends 1 ? (F extends (a: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 2 ? (F extends (a: any, b: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 3 ? (F extends (a: any, b: any, c: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 4 ? (F extends (a: any, b: any, c: any, d: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 5 ? (F extends (a: any, b: any, c: any, d: any, e: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 6 ? (F extends (a: any, b: any, c: any, d: any, e: any, f: any, ...args: infer TArgs) => any ? TArgs : never) :
  never

type TailArray<A extends any[], S extends Number> =
  Tail<(...args: A) => any, S>

type Args<T extends Function> =
  T extends (...args: infer TArgs) => any ? TArgs
  : never

type PartialArgs<T extends Function> =
  T extends (...args: infer TArgs) => any ? Partial<TArgs>
  : never

type Curried<T extends (...args: any) => any, TReturn = ReturnType<T>> =
  <
    TArgs extends PartialArgs<T>,
    TRest extends TailArray<Args<T>, TArgs['length']>
  >(...args: TArgs) =>
    TRest extends []
      ? TReturn
      : Curried<(...args: TRest) => TReturn>

type Curry = <TFunc extends (...args: any) => any>(func: TFunc) => Curried<TFunc>

declare const curry: Curry

const curried = curry((a: 1 | undefined, b: number | number[], c: string) => 1)

// works :D
const a = curried(1)([2])('x')
const b = curried(1)(2, 'x')
const c = curried(1, 2)('x')
const d = curried(1, 2, 'x')


// the only problem is that `undefined` is accepted
// Partial<[1, 2]> => [1 | undefined, 2 | undefined]
curried(undefined)(2)(undefined)

I got it, real variadic curry, thanks tuple types:

type Init<T extends any[], TTail extends any[] = TailArray<T>> = CastArray<{
  [K in keyof TTail]: T[keyof T & K];
}>

type PotentialArgs<T extends any[], TResult extends any[] = T> = {
  "continue": PotentialArgs<Init<T>, TResult | Init<T>>;
  "end": TResult;
}[T extends [] ? "end" : "continue"]

type Args<T extends Function> =
  T extends (...args: infer TArgs) => any ? TArgs
  : never

type TailArgs<F extends Function> =
  F extends (head: any, ...tail: infer TTail) => any ? TTail :
  never

type TailArray<A extends any[]> = TailArgs<(...args: A) => any>

type CastArray<T> = T extends any[] ? T : []

type DropFromArraySize<TSource extends any[], TSize extends any[]> = CastArray<{
  "continue": DropFromArraySize<TailArray<TSource>, TailArray<TSize>>,
  "end": TSource,
}[TSize extends [] ? "end" : "continue"]>

type PartialArgs<T extends Function> =
  T extends (...args: infer TArgs) => any ? Partial<TArgs>
  : never

type Curried<T extends (...args: any) => any> =
  <
    TInitArgs extends PotentialArgs<Args<T>>,
    TTailArgs extends DropFromArraySize<Args<T>, TInitArgs>
  >(...args: TInitArgs) =>
    TTailArgs extends []
      ? ReturnType<T>
      : Curried<(...args: TTailArgs) => ReturnType<T>>

type Curry = <TFunc extends (...args: any) => any>(func: TFunc) => Curried<TFunc>

declare const curry: Curry

const curried = curry((a: 1 | undefined, b: number | number[], c: string) => 1)

// works :D
const a = curried(1)([2])('x')
const b = curried(1)(2, 'x')
const c = curried(1, 2)('x')
const d = curried(1, 2, 'x')
const e = curried(undefined)(2)('2')

curried(undefined)(2)(undefined) // notify error
type DropFromArraySize<TSource extends any[], TSize extends any[]> = CastArray<{
 "continue": DropFromArraySize<TailArray<TSource>, TailArray<TSize>>,
 "end": TSource,
}[TSize extends [] ? "end" : "continue"]>

Hi, just your friendly neighborhood killjoy reminding everyone that this kind of back-door recursive conditional type is officially frowned upon* and can be expected to make your compiler very angry.
Further discussion about this should probably happen in #26980 instead of here.


* Getting that link to work seems to be a crapshoot. It's a long thread with the relevant comment hidden by default. Sometimes that link will expand the comments automatically, sometimes it will just unhelpfully bring you to the top of the thread. If you unhide hidden comments you will eventually see the one I'm referencing, included here for your convenience:

@ahejlsberg said:

unless someone like @ahejlsberg can tell us if we can expect things like that to keep working or not

It's clever, but it definitely pushes things well beyond their intended use. While it may work for small examples, it will scale horribly. Resolving those deeply recursive types consumes a lot of time and resources and might in the future run afoul of the recursion governors we have in the checker.

Don't do it!

@jcalz Thank you for the warning. In my tests, there were no performance issues with DropFromArraySize, but PotentialArgs made the type resolution slower (I did not know if this had to do with TSServer or some detail of TS itself).

Not sure if this still counts as a backdoor recursion, but by using an interface in the reduction step it is possible to recurse without potentially crashing the compiler (at least in my tests):

// Tuple operations Cons, Head, Tail

type Head<T> = T extends [infer U, ...unknown[]] ? U : never

type Tail<T> = T extends Array<any>
  ? ((...args: T) => never) extends ((a: any, ...args: infer R) => never)
    ? R
    : never
  : never

type Cons<T extends any[], H> = ((h: H, ...t: T) => any) extends ((...x: infer X) => any) ? X : never

// Generic lazy tuple reduction

interface Reduction<Base, In> {
  0: Base
  1: In
}

type Reduce<T extends Array<any>, R extends Reduction<any, any>> = R[[T] extends [[]] ? 0 : 1]

// Tuple reversal

interface ReverseRec<H extends Array<any>, T extends Array<any>>
  extends Reduction<H, Reduce<Tail<T>, ReverseRec<Cons<H, Head<T>>, Tail<T>>>> {}

type Reverse<T> = [T] extends [Array<any>] ? Reduce<T, ReverseRec<[], T>> : never

// Currying, finally

interface CurryRec<H, T extends Array<any>>
  extends Reduction<H, Reduce<Tail<T>, CurryRec<(...args: [Head<T>]) => H, Tail<T>>>> {}

type Curry<F extends (...args: any[]) => any> = Reverse<Parameters<F>> extends infer R
  ? R extends any[]
    ? Reduce<R, CurryRec<ReturnType<F>, R>>
    : never
  : never

declare function curry<F extends (...args: any[]) => any>(f: F): Curry<F>
declare const f: (a: number, b: string, c: symbol, d: boolean, e: void) => boolean

const curried = curry(f) // (args_0: number) => (args_0: string) => (args_0: symbol) => (args_0: boolean) => (args_0: void) => boolean

(Playground link)

It seems that even if you specify a recursion interface that would never terminate (example), the compiler does not crash since interfaces are not eagerly evaluated. I also implemented variadic compose using Reduction and Reduce without any visible compilation performance hit.

@DanielRosenwasser thanks for your solution! But I'm a bit confused...

type WrappedTuple = { [K in keyof T]: Wrap<T[K]> } returns a tuple and not an object?!

I would expect to see type WrappedTuple = {0: Wrap<T1>, 1: Wrap<T2>, 2: Wrap<T3>}.

I see the thinking that these are basically equivalent right? Well then how come this doesn't work?

function f(args: ["a", "b"]) {}
f({0: "a", 1: "b"})

@ccorcos This was the way the mapped types were extended a while back https://github.com/Microsoft/TypeScript/pull/26063

@strax Cool, thanks for sharing! Unfortunately, the snippet does not type check anymore (>= v3.5.1). I am mainly interested since it's the only Reduce implementation I've found on the web, or is it included in some utility helpers with another name?

Was this page helpful?
0 / 5 - 0 ratings