Search code examples
typescript

Array.prototype.sort() does not preserve element types for tuples


Simple sample works as expected:

type Data = {
    a: number
    b: number
}

const data: Data[] = [
    {a :1, b: 1},
    {a: 2, b: 2}
].sort((el1, el2) => el1.a - el2.a)

sorting works, type is preserved.

But it does not work for array of tuples:

type Data = [number, number]

const data: Data[] = [ // error: Type number[][] is not assignable to type Data[]
    [1, 2],
    [3, 4]
].sort((a, b) => a[0] - b[0])

And with heterogeneous items it's even worse:

type Data = [number, boolean]

const data: Data[] = [ // error: Type '(number | boolean)[][]' is not assignable to type 'Data[]'
    [1, true],
    [3, false]
].sort((a, b) => a[0] - b[0])
//      ^? (parameter) a: (number | boolean)[]

Are my types conceptually wrong? Or is it a known limitation and, if yes, is there a workaround?

I know .sort() mutates an array. But since top-level is not a tuple, I'd expect TypeScript preserve result type and properly infer element's type inside of .sort().


Solution

  • In order for

    const data: Data[] = [[1, 2], [3, 4]].sort((a, b) => a[0] - b[0]);
    

    to work as-is, TypeScript would need to infer the type of the array literal by contextually propagating the expected return type of Data[] back through the call to sort(), in order to see that [[1, 2], [3, 4]] must also be Data[]. But contextual type inference doesn't consistently reach back through method calls that way. It's a limitation of the language. Similar issues can be seen at microsoft/Typescript#29771 and microsoft/TypeScript#47440.

    What happens instead is that [[1, 2], [3, 4]] ends up getting the default non-contextual inferred type of number[][], and then sort() returns number[][], and the assignment fails.


    This can be worked around in any number of ways all of which involve the developer specifying more explicit types rather than relying on inference.

    My suggestion here would be to give [[1, 2], [3, 4]] a contextual type of Data[] by using the satisfies operator:

    const data: Data[] = ([
      [1, 2],
      [3, 4]
    ] satisfies Data[]).sort((a, b) => a[0] - b[0]) // okay
    

    You could also just write

    const data = ([
      [1, 2],
      [3, 4]
    ] satisfies Data[]).sort((a, b) => a[0] - b[0]) // okay
    

    and the inferred type of data will be equivalent to Data[]. Both of these versions have the advantage of not changing your runtime code (plus or minus some parentheses) and being relatively type safe (as opposed to using the as type assertion operator).

    Playground link to code