Why typescript tuples allow array.push method? [duplicate]
Asked Answered
N

1

6

In the following code snippet which from a TypeScript file, I have defined role to be of Tuple type, therefore only 2 values of specified type should be allowed in role array.

But I am able to push a new item, why can TS compiler not prevent it ?

let person: {
    name: string;
    age: number;
    hobbies: string[];
    role: [number, string]      //tuple type
} = {
    name: 'stack overflow',
    age: 30,
    hobbies: ['reading', 'jogging'],
    role: [2, 'author']
};

person.role[2] = 'reader';  //not allowed, which is as expected
person.role.push('reader'); //allowed, TS compiler should prevent it
Nonbeliever answered 7/6, 2022 at 9:23 Comment(3)
Because to do otherwise would break too much stuff, basically, see e.g. github.com/microsoft/TypeScript/issues/6325.Preview
You need to use readonly [string, string].Seldan
If we use readonly then that value cannot be changed at all. person.role[1] = 'reader'; fails to compile. That's not the intention. The idea is to prevent compile time issue.Nonbeliever
S
1

In order to do that, we need to create a generic utility type wich will create a tuple with expected length but without mutable Array.prototype methods.

Consider this:

type WithoutIndex<T> = Omit<T,number>


type MutableProps = 'push' | 'splice' | 'shift' | 'unshift' | 'pop' | 'sort'

type Tuple<
    Length extends number,
    Type,
    Result extends Type[] = []
    > =
    (Result['length'] extends Length
        ? WithoutIndex<Omit<Result, MutableProps>>
        : Tuple<Length, Type, [...Result, Type]>)

Tuple calls itself until length of Result will be the same as provided Length argument. Here you have js representation:

const Tuple = (length: number, result: number[] = []) => {
    if (length === result.length) {
        return result
    }
    return tuple(length, [...result, 1])
}

WithoutIndex just forbids using any numeric indexes which are not exist in tuple.

Lets try if it works:

type WithoutIndex<T> = Omit<T, number>

type MutableProps = 'push' | 'splice' | 'shift' | 'unshift' | 'pop' | 'sort'

type Tuple<
    Length extends number,
    Type,
    Result extends Type[] = []
    > =
    (Result['length'] extends Length
        ? WithoutIndex<Omit<Result, MutableProps>>
        : Tuple<Length, Type, [...Result, Type]>)


let person: {
    name: string;
    age: number;
    hobbies: string[];
    role: Tuple<2, string>
} = {
    name: 'stack overflow',
    age: 30,
    hobbies: ['reading', 'jogging'],
    role: ['reading', 'jogging'],
};

person.role[1] = 'reader';  //ok
person.role[10] = 'reader';  // expected error
person.role.push(); //allowed, TS compiler should prevent it
person.role.sort // expected error

Playground

Diversity tuple

type WithoutIndex<T> = Omit<T, number>

type MutableProps = 'push' | 'splice' | 'shift' | 'unshift' | 'pop' | 'sort'

type DiversityTuple<T extends readonly any[]> = WithoutIndex<Omit<T, MutableProps>>

const diffTUple: DiversityTuple<[string, number]> = ['hello', 42]

diffTUple[0] = 'a' // ok
diffTUple[1] = 1 // ok

diffTUple[10] = 1 // expected error
diffTUple[0] = 1 // expected error
diffTUple[1] = 'b' // expected error


Seldan answered 7/6, 2022 at 11:59 Comment(2)
Good attempt @captain-yossarian from Ukraine, just one question, will this support a Tuple which has 2 types ? Example: [number, string]Nonbeliever
@Nonbeliever sure, made an updateSeldan

© 2022 - 2024 — McMap. All rights reserved.