Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use array.map with tuples in typescript?

Whenever I use array.map on a tuple, Typescript infers it as a generic array. For instance, here are some pieces of a simple 3x3 sudoku game:

const _ = ' ' // a "Blank"

type Blank = typeof _

type Cell = number | Blank

type Three = [Cell, Cell, Cell]

type Board = [Three, Three, Three]

const initialBoard: Board = [
    [_, 1, 3],
    [3, _, 1],
    [1, _, _],
]

// Adds a `2` to the first cell on the first row
function applyMove(board: Board): Board {
    // đŸ‘‡errors here
    const newBoard: Board =  board.map((row: Three, index: number) => {
        if (index === 0) return <Three> [2, 1, 3]
        return <Three> row
    })
    return newBoard
}

function applyMoveToRow(row: Three): Three {
    // return [2, 1, 3] // This works
    const newRow: Three = [
        2,
        ...row.slice(1, 3)
    ]
    return newRow
}

The TS error is:

Type '[Cell, Cell, Cell][]' is missing the following properties from type 
 '[[Cell, Cell, Cell], [Cell, Cell, Cell], [Cell, Cell, Cell]]': 0, 1, 2 .  

here it is in a TS Playground.

Is there any way to tell typescript that, when I'm mapping over a tuple, it's going to return a tuple of the same kind, instead of just an array? I've tried being very explicit, annotating all of my return values, etc, but it's not doing the trick.

There's a discussion on the Typescript github about this: https://github.com/Microsoft/TypeScript/issues/11312

But I haven't been able to get a solution out of it.

like image 561
Good Idea Avatar asked Sep 12 '19 19:09

Good Idea


People also ask

How do you declare an array of tuples in TypeScript?

You can declare an array of tuple also. var employee: [number, string][]; employee = [[1, "Steve"], [2, "Bill"], [3, "Jeff"]]; TypeScript generates an array in JavaScript for the tuple variable. For example, var employee: [number, string] = [1, 'Steve'] will be compiled as var employee = [1, "Steve"] in JavaScript.

Does TypeScript have tuples?

TypeScript gives us a data type called tuple that helps to achieve such a purpose. It represents a heterogeneous collection of values. In other words, tuples enable storing multiple fields of different types. Tuples can also be passed as parameters to functions.

What is the difference between tuple and array in TypeScript?

The structure of the tuple needs to stay the same (a string followed by a number), whereas the array can have any combination of the two types specified (this can be extended to as many types as is required).

What is array map in TypeScript?

The Array. map() is an inbuilt TypeScript function that is used to create a new array with the results of calling a provided function on every element in this array.


1 Answers

TypeScript does not attempt to preserve tuple length upon calling map(). This feature was requested in microsoft/TypeScript#11312, implemented in microsoft/TypeScript#11252, and reverted in microsoft/TypeScript#16223 due to problems it caused with real world code. See microsoft/TypeScript#29841 for details.

But if you want, you could merge in your own declaration for the signature of Array.prototype.map(), to account for the fact that it preserves the length of tuples. Here's one way to do it:

interface Array<T> {
  map<U>(
    callbackfn: (value: T, index: number, array: T[]) => U,
    thisArg?: any
  ): { [K in keyof this]: U };
}

This uses polymorphic this types as well as array/tuple mapped types to represent the transformation.

Then your code could be written like the following:

function applyMove(board: Board): Board {
  return board.map(
    (row: Three, index: number) => (index === 0 ? applyMoveToRow(row) : row)
  );
}

function applyMoveToRow(row: Three): Three {
  return [2, row[1], row[2]];
}

and there'd be no error. Note that I didn't bother trying to deal with Array.prototype.slice(). It would be a large amount of effort to try to represent what slice() does to a tuple type, especially since there's no real support for tuple length manipulation... meaning you might need a bunch of overload signatures or other type trickery to get it done. If you're only going to use slice() for short arrays, you might as well just use index access like I did above with [2, row[1], row[2]] which the compiler does understand.

Or if you're going to use it for longer arrays but a small number of times in your code, you might just want to use a type assertion to tell the compiler that you know what you're doing. For that matter, if you're only doing map() a small number of times, you can use a type assertion here too instead of the above redeclaration of map()'s signature:

function applyMove(board: Board): Board {
  return board.map(
    (row: Three, index: number) => (index === 0 ? applyMoveToRow(row) : row)
  ) as Board; // assert here instead of redeclaring `map()` method signature
}

Either way works... type assertions are less type safe but more straightforward, while the declaration merge is safer but more complicated.

Hope that helps; good luck!

Link to code

like image 169
jcalz Avatar answered Oct 21 '22 20:10

jcalz