Repository navigation
Type tuples no longer inferred properly from rest arguments with mapped generic type tuples. #49556
Description
I recently updated the dependencies for a TypeScript-based project and unfortunately ended up with some unexpected breakage.
The TypeScript version was changed from 4.6 to 4.7 and type tuples are no longer inferred properly from rest arguments with mapped generic type tuples.
type MyMappedType<Primitive extends any> = {
primitive: Primitive;
};The project uses several helper types to map type tuples to other type tuples as shown below.
type TupleMapperOld<Tuple extends any[]> = {
[Key in keyof Tuple]: Tuple[Key] extends Tuple[number] ? MyMappedType<Tuple[Key]> : never;
};After updating to TypeScript 4.7 it is still possible to use the helper directly. TypeScript will correctly infer the type as expected.
// Type should be [MyMappedType<string>, MyMappedType<number>] in TypeScript 4.6 and TypeScript 4.7.
type MyMappedTupleOld = TupleMapperOld<[string, number]>;However, type inference breaks when using the mapper to map the rest arguments of a generic function as shown below.
function extractPrimitivesOld<Tuple extends any[]>(...mappedTypes: TupleMapperOld<Tuple>): Tuple {
return mappedTypes.map((mappedType) => mappedType.primitive) as Tuple;
}
// Inferred type should be [string, number] but becomes [unknown, unknown] in TypeScript 4.7.
const myPrimitiveTupleOld = extractPrimitivesOld({ primitive: "" }, { primitive: 0 });The problem can be mitigated by explicitly specifying the generic type tuple argument. It can also be solved by removing the mapping constraints in the mapper as shown below.
type TupleMapperNew<Tuple extends any[]> = {
[Key in keyof Tuple]: MyMappedType<Tuple[Key]>;
};I decided to open this issue and let you decide if this is something worth investigating since I couldn't find any information about the difference in behaviour in the release notes.
🔎 Search Terms
tuple mapping
type inference
rest arguments
generics
🕗 Version & Regression Information
This changed between versions 4.6 and 4.7.
⏯ Playground Link
💻 Code
type MyMappedType<Primitive extends any> = {
primitive: Primitive;
};
type TupleMapperOld<Tuple extends any[]> = {
[Key in keyof Tuple]: Tuple[Key] extends Tuple[number] ? MyMappedType<Tuple[Key]> : never;
};
// Type should be [MyMappedType<string>, MyMappedType<number>] in TypeScript 4.6 and TypeScript 4.7.
type MyMappedTupleOld = TupleMapperOld<[string, number]>;
function extractPrimitivesOld<Tuple extends any[]>(...mappedTypes: TupleMapperOld<Tuple>): Tuple {
return mappedTypes.map((mappedType) => mappedType.primitive) as Tuple;
}
// Inferred type should be [string, number] but becomes [unknown, unknown] in TypeScript 4.7.
const myPrimitiveTupleOld = extractPrimitivesOld({ primitive: "" }, { primitive: 0 });
// ^?
type TupleMapperNew<Tuple extends any[]> = {
[Key in keyof Tuple]: MyMappedType<Tuple[Key]>;
};
// Type should be [MyMappedType<string>, MyMappedType<number>] in TypeScript 4.6 and TypeScript 4.7.
type MyMappedTupleNew = TupleMapperNew<[string, number]>;
function extractPrimitivesNew<Tuple extends any[]>(...mappedTypes: TupleMapperNew<Tuple>): Tuple {
return mappedTypes.map((mappedType) => mappedType.primitive) as Tuple;
}
// Inferred type should be [string, number] in TypeScript 4.6 and TypeScript 4.7.
const myPrimitiveTupleNew = extractPrimitivesNew({ primitive: "" }, { primitive: 0 });🙁 Actual behavior
The type tuple is inferred as [unknown, unknown] in TypeScript 4.7.
🙂 Expected behavior
The type tuple is inferred as [string, number] in TypeScript 4.7.
Activity
andrewbranch commentedon Jul 6, 2022
TypeScript Bot (@typescript-bot) bisect good v4.6.4 bad v4.7.4
typescript-bot commentedon Jul 6, 2022
The change between v4.6.4 and v4.7.4 occurred at 787bb9d.
andrewbranch commentedon Jul 6, 2022
What was the purpose of this conditional type? At a glance it seems like it should always evaluate true.
typescript-bot commentedon Jul 7, 2022
👋 Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of the repro in the issue body running against the nightly TypeScript.
Issue body code block by Joel Ek (@joelek)
const myPrimitiveTupleOld: [unknown, unknown]Historical Information
const myPrimitiveTupleOld: [unknown, unknown]const myPrimitiveTupleOld: [string, number]jakebailey commentedon Jul 7, 2022
So, at first glance, the above appears to be intended to filter the keys of a tuple type to only the numeric indexes (so, filter out any named things like if you had
any[] & { foo: number }, except that it seems to not really work correctly because if you have[string, number] & { foo: number },foowouldn't be filtered out because it has a value type ofnumber(Tuple['foo']) and that is one of the values of the indexed types (Tuple[number]isstring | number). It would seem like the right thing to write would be:This doesn't doesn't compile and has a confusing error at the call.
But, depending on what we think
keyof Tupleis supposed to be comprised of, this would feel like it should always be true (and that is in fact what the "New" variants in the OP are doing).#48837 seems to be intended to change indexed accesses to arrays/tuples to not only be
numberbut also`${number}`. So, if you change the above such that it is still always true (presumably, #48837 was a breaking change in that way), you can write:And this compiles with the original example in 4.7, so maybe it's a workaround?
If we go back to the original example, it would seem like we could make the same transformation by adding
`${number}`and write:Except that we get an error that says that
Tuplecannot be indexed by`${number}`, which is what #48837 seems to have been intended to change, so that feels incongruent (as though there's some other part of the checker that also needs to be updated to allow this). I'm not sure if that's the entirety of the problem or if it's that + the fact that #48837 sort of broke the assumptions that code can make about what keys can be in arrays/tuples.All examples better laid out here: Playground Link
andrewbranch commentedon Jul 7, 2022
I think the upshot of this is that the purpose of the conditional type was to do exactly what #48837 does unconditionally, so it can just be removed. The conditional type is a workaround for the previous weird behavior of mapping over generic tuples/arrays. That said, I think the lost inference in the original example is still probably a bug.
jakebailey commentedon Jul 7, 2022
Just to complete my side investigation, the "Type '
`${number}`' cannot be used to index type 'Tuple'." message is caused by our definition of the index type forArray, which is defined as[n: number]: T; changing this ton: number | `${number}`]: Tmakes the error go away, but does not fix the inferencing problem reported in this issue, so I doubt it is the cause like you say. (It also breaks a number of other things in mysterious ways, so I doubt it is a desirable change anyhow, at least not the prototype I tested.)joelek commentedon Jul 8, 2022
Helpers like the conditionally mapped type
TupleMapperOldis what you find when you search the web for type tuple mapping in TypeScript even though it no longer seems to be needed.I started using the pattern a few years ago due to having issues with unwanted mapping over non-numeric keys and I believe that the pattern is somewhat well-established in code bases. The fix is simple, just remove the conditional, but the weird behavior of lost inference in certain circumstances might leave people puzzled and could, as you say, indicate a bug.
jakebailey commentedon Feb 19, 2023
This seems to have turned out to just be a bug; see #52848.