question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Type tuples no longer inferred properly from rest arguments with mapped generic type tuples.

See original GitHub issue

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

Playground 4.6.4

Playground 4.7.2

💻 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.

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:2
  • Comments:8 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
typescript-botcommented, Jul 6, 2022

The change between v4.6.4 and v4.7.4 occurred at 787bb9ddb60368f84b1ed1b0971aa2bf79fdec77.

1reaction
joelekcommented, Jul 8, 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.

Helpers like the conditionally mapped type TupleMapperOld is 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Inferring a generic tuple with spread expressions in typescript
When you call a generic function without manually specifying its type parameters, the compiler needs to infer these type parameters, ...
Read more >
4. Functions - Programming TypeScript [Book]
Chapter 4. Functions In the last chapter we covered the basics of TypeScript's type system: primitive types, objects, arrays, tuples, and enums, ...
Read more >
Documentation - Everyday Types
In this chapter, we'll cover some of the most common types of values you'll find in JavaScript code, and explain the corresponding ways...
Read more >
Arrays & Tuples
Since the introduction of Variadic Tuples, we can use the ... rest element syntax to mix Arrays and Tuples. This allows us to...
Read more >
Experimenting with TypeScript 4.0's Variadic Tuple Types ...
The core issue is that we're mapping over a tuple type and so TypeScript just can't guess that the return type of map...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found