Here's a question worth pondering, and I'll do my best to articulate it clearly:
Tuple types are arrays with a specified length where elements can have different data types. For example, a type of [number, string]
ensures a length of 2
, with a number
at index 0
and a string
at index 1
.
However, why does TypeScript allow methods such as push()
, pop()
, shift()
, unshift()
, and splice()
on tuple types when these methods contradict the intended guarantees of tuple types? Shouldn't TypeScript prevent these actions just like it prevents assigning [1, "two", "three"]
to [number, string]
initially?
It's definitely not ideal.
There isn't a definitive answer to this dilemma. One solution proposed in microsoft/TypeScript#6325 suggests excluding these methods from tuple types, but it was rejected due to potential disruptions to existing codebases according to this discussion.
An alternative approach looks something like this:
type StrictTuple<T extends any[]> =
Omit<T, keyof (any[])> extends infer O ? { [K in keyof O]: O[K] } : never;
This alternative represents more of a set of properties with numeric keys than an array:
const x: StrictTuple<[number, string]> = [1, ""]
x[1] = "okay";
x[0] = 123;
x.push(123); // error!
//~~~~ Property 'push' does not exist on type { 0: number; 1: string; }
If you're concerned about maintaining strict tuple types, using something like StrictTuple
might help, but it could add unnecessary complexity. Since tuple types are widely used in TypeScript, deviating from their standard form may lead to significant challenges in utilizing TypeScript effectively.
Practically speaking, avoiding mutations on tuples is likely the simplest solution for now.
Recent discussions in issues like microsoft/TypeScript#40316 and microsoft/TypeScript#48465 suggest reconsidering omitting push
, pop
, and similar methods from tuple types, although no clear consensus has emerged. It would be intriguing to explore the repercussions if these methods were excluded from tuple types.
Check out the code snippet here