-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Mapped tuple types #25947
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 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 |
If a "tuple-like" object (sequential "numeric" keys starting from 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 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 type M1 = [ [K in keyof T & Index]-?: T[K] ]; Or a new operator |
I think you can get just the "numeric" keys by doing That being said, I think that we adopt the |
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:
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 Would this new feature help in this case? |
@xaviergonz Can't you just use |
@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 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
right now the only solution I found for the latest is to have a million overloads like
|
@xaviergonz No, once the array is assigned to a variable, its type is |
@mattmccutchen ah I see, still much better than any other solution I had before, thanks for the help! |
@DanielRosenwasser 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 :) |
Following to feature Mapped tuple types microsoft#25947 It is now possible to have better typings for Promise.race
Following the feature - Mapped tuple types microsoft#25947 - it is now possible to have better typings for Promise.race
Following the feature - Mapped tuple types microsoft#25947 - it is now possible to have better typings for Promise.race
this functionality has helped a lot to work with something similar to 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 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 |
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. * 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:
|
@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 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 |
Following the feature - Mapped tuple types microsoft#25947 - it is now possible to have better typings for Promise.race
Following the feature - Mapped tuple types #25947 - it is now possible to have better typings for Promise.race
@DanielRosenwasser thanks for your solution! But I'm a bit confused...
I would expect to see 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"}) |
@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 |
@ccorcos Hello! This works: function f(args: ["a", "b"]) {}
f({0: "a", 1: "b"} as ['a', 'b']) The plain object's type is missing the properties that an Array has, so TS doesn't see it as an array (structural types). I think. |
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:
This would apply a type mapping to each of the element types of the tuple.
For example:
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:
Examples
The basic usage of this would be as follows:
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:Similarly RxJS'
combineLatest
would similarly benefit (Note: I've moved theproject
argument to the head of the list because rest arguments must appear last, this differs from the current definition ofcombineLatest
)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:
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:
The text was updated successfully, but these errors were encountered: