32
const testArray:[number, string] = [10, 'test', 's'];

It doesn't work.

const testArray:[number, string] = [10, 'test']; // It's been edited.
testArray.push('test');

It works.

I think second code shouldn't worked.
Why does the second code work? Is it a bug?


-Added-

I've been thinking about the problem.
Compiler can catch errors only in compile time. So the compiler doesn't catch the error. Is it right?

3
  • How would you expect let testArray:[number, string]; behave? It's undefined upon declaration. Commented Sep 25, 2020 at 18:34
  • 1
    @Taplar I understand why the first code doesn't work. so I thought the second one also doesn't work. that's why I posted this question. Commented Sep 25, 2020 at 18:50
  • Update:-- After February 2021 (TS 4.2 and newer) → tuple type checking became strict, .push() only allows valid tuple element types. ex type User = [number, string]; newUser.push(boolean); // error in 4.2+ Commented Nov 1 at 9:56

4 Answers 4

36

This is a good question, and I'll try to rephrase it as explicitly as possible:

Tuple types are a type of array of known length and where the different elements may have different types. A value of type [number, string] is guaranteed to have a length of 2, with a number at element 0 and a string at element 1.

Why then, does TypeScript allow you to call methods like push(), pop(), shift(), unshift(), and splice() on values of tuple types, when such methods generally destroy the supposed guarantees of tuple types? Shouldn't it stop you from doing that, just like it stops you from assigning a value like [1, "two", "three"] to [number, string] in the first place?

Yeah, it's not great.

I don't know that there's a good canonical answer to this. The closest I can find is microsoft/TypeScript#6325, which proposes that such methods be omitted from tuple types. This proposal was declined, on the possible grounds that it would be a breaking change for existing real-world code.

A suggested alternative looks like

type StrictTuple<T extends any[]> =
    Omit<T, keyof (any[])> extends infer O ? { [K in keyof O]: O[K] } : never;

which looks less like an array and more like a set of numeric-key properties:

const x: StrictTuple<[number, string]> = [1, ""] // {0: number; 1: string }
x[1] = "okay";
x[0] = 123;
x.push(123); // error!
//~~~~ Property 'push' does not exist on type { 0: number; 1: string; }

If you really care about such things, you might want to use something like StrictTuple above, but it's probably more trouble than it's worth. Tuple types are ubiquitous in TypeScript and if you use a form that is not assignable to them, then you will have to jump through a lot of unfortunate hoops to use TypeScript.

Pragmatically speaking, I'd say just try not to mutate tuples.

There have been some newer issues referencing microsoft/TypeScript#6325 asking to reconsider this now that tuples have gotten a little more strict in the intervening time. See microsoft/TypeScript#40316 and microsoft/TypeScript#48465, which are open but not obviously popular. It would be an interesting exercise to see what specifically would break if push/pop/etc were omitted from tuple types.

Playground link to code

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for the answer. It helps a lot.
@seongkukhan This is the real answer to the question. You should mark it as the answer to your question.
Update:-- After February 2021 (TS 4.2 and newer) → tuple type checking became strict, .push() only allows valid tuple element types. ex type User = [number, string]; newUser.push(boolean); // error in 4.2+
But you don’t want to allow pushing anything at all. It’s not the type, but the length that’s the issue.
10

[Removed previous answer since I misinterpreted.]

Looks like this could be an issue related to type widening? You can declare your tuple to be readonly or as const to prevent the push function from working on the tuple at all

const testArray:readonly [number, string] = [10, 'test'] as const;
testArray.push('test') // error

I found this medium article on the topic: https://blog.logrocket.com/const-assertions-are-the-killer-new-typescript-feature-b73451f35802/

4 Comments

Thank you for the answer. but that's not answer I want. I'm sorry. I think I wrote the question complicatedly.
I updated the answer to hopefully reflect your actual question :)
readonly actually does prevent the mutation. That's a cute trick!
readonly prevents setting the numeric indices too; testArray[1] = "str"; would be an error; not sure if that's what is actually desired here.
2

Your types don't match what you're trying to feed it.

// Error: Only two elements can be in the tuple
// First index must be a number
// Second index must be a string
const testArray:[number, string] = [10, 'test', 's'];

// Should be this instead
const testArray:[number, string, string] = [10, 'test', 's'];

// Error: Can't initialize value with an empty array
// Needs to provide [number, string] or nothing
const testArray:[number, string] = [];
// Error: You're trying to push a string
// when it's expecting [number, string]
testArray.push('test');

// Should be this
const testArray:[number, string] = [0, 'test'];
testArray.push(1, 'test');

https://www.typescriptlang.org/docs/handbook/basic-types.html#tuple

8 Comments

I understand why the first code doesn't work because It doesn't match to the type. but I thought second one also doesn't work because If I push 'test' to the empty array. It won't be matched of the tuple type. but It works. I think It's not the problem in compilation time. that's why the second code works. Is it right?
well I dont think testArray.push(1, 'test'); should be ok because when you do it you try to say that this array should only contain 2 indexes one of string and one of number
If you do testArray.push('test'), you are trying to feed a string value to a tuple type of number, string. You can't do that. ...well at tarnspile time at least. TS doesn't matter at runtime.
``` const a:[number, string] = [10, 'st']; a.push('s'); console.log(a); ``` I coded above code and then ran. It's worked without no errors. I tested this code on an online compiler. 'typescriptlang.org'.
That may be true, but it invalidates the object itself since you're saying everything is [number, string] but you just pushed a string. So now the format is off. Also, an error such as this would not be caught at runtime, only when the TS compiler runs. Runtime has no clue what TS is.
|
0

If you define the tuple this way it will throw an error

const testArray: Array<[number, string]> = [10, 'test'];   // Correct
const testArray: Array<[number, string]> = [10, 'test', 's']; //Error

const testArray: Array<[number, string]> = [10, 'test']; /Error
const testArray: Array<[number, string]> = [[10,'test']]; //Correct

testArray.push('test'); //Error
testArray.push([11,'test']) //Correct

1 Comment

Lines 1 and 3 are the same in your answer, but you're saying that 1 is correct while 3 throws an error?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.