1

I have an array of arrays (e.g.

const input = [['A', 'X'], ['B', 'Y'],...];

). I also have two enums:

  enum MyMove {
    Rock = 'X',
    Paper = 'Y',
    Scissors = 'Z',
  };

  enum OpponentMove {
    Rock = 'A',
    Paper = 'B',
    Scissors = 'C',
  };

I want to run a Array.prototype.reduce() on input, but I can't figure out how to annotate callbackFn.currentValue. I want to do something like this:

  const score: number = input.reduce(
    (acc: number, ([opponentMove, myMove]: [OpponentMove, MyMove])) => {

(based on Types when destructuring arrays), but I'm getting the following errors:

02/index.ts:38:9 - error TS2322: Type 'string[]' is not assignable to type 'number'.

38   const score: number = input.reduce(
           ~~~~~
02/index.ts:39:21 - error TS2304: Cannot find name 'opponentMove'.

39     (acc: number, ([opponentMove, myMove]: [OpponentMove, MyMove])) => {
                       ~~~~~~~~~~~~
02/index.ts:39:35 - error TS2304: Cannot find name 'myMove'.

39     (acc: number, ([opponentMove, myMove]: [OpponentMove, MyMove])) => {
                                     ~~~~~~
02/index.ts:39:44 - error TS2554: Expected 1-2 arguments, but got 3.

39     (acc: number, ([opponentMove, myMove]: [OpponentMove, MyMove])) => {
                                              ~~~~~~~~~~~~~~~~~~~~~~

What is the proper way to annotate [opponentMove, myMove]?

neverendingqs
  • 4,006
  • 3
  • 29
  • 57
  • I don't understand why you need to annotate at all... you didn't show the whole callback, but [it should just work as is](https://tsplay.dev/w2p5rm) without annotations. Or if you must annotate you could do it [this way](https://tsplay.dev/NB4JxW). Do either of those meet your needs? If so I could write up an answer explaining; if not, what am I missing? – jcalz Dec 10 '22 at 04:01
  • @jcalz I think I got it, and I think it's related to your question. Instead of annotating inside `callbackFn.currentValue`, I can just cast `input` from `String[][]` to `Array<[OpponentMove, MyMove]>`. – neverendingqs Dec 11 '22 at 22:39

1 Answers1

0

Instead of annotating the callback parameter types in your destructuring assignment, you should probably properly annotate the type of the input variable in the first place. For example:

const input: Array<[OpponentMove, MyMove]> = [
  [OpponentMove.Rock, MyMove.Rock],
  [OpponentMove.Paper, MyMove.Paper]
];

Then the destructured callback parameters to reduce() should be inferred from context automatically:

const score: number = input.reduce((
  acc, [opponentMove, myMove]) => 2, 0);
// (parameter) opponentMove: OpponentMove
// (parameter) myMove: MyMove

Which is what you wanted.


Note that I changed the initialization of input from your version, to avoid problems:

const input: Array<[OpponentMove, MyMove]> = 
  [['A', 'X'], ['B', 'Y']]; // error!

When you have an enum, you should only refer to the value through the enum itself. While, for example, MyMove.Rock and "X" are the same value at runtime, the TypeScript compiler treats MyMove.Rock as a special version of "X" and gets upset if you try to use the latter when it expects the former:

const oops: MyMove = "X"; // error! Type '"X"' is not assignable to type 'MyMove'.
const okay: MyMove = MyMove.Rock; // okay

And while you could use a type assertion to "cast" the latter to the former, it is unsafe:

const assert = "X" as MyMove; // okay
const whaa = "x" as MyMove; // still okay to the compiler, but this is a mistake

So you should be writing this:

const input: Array<[OpponentMove, MyMove]> = [
  [OpponentMove.Rock, MyMove.Rock],
  [OpponentMove.Paper, MyMove.Paper]
];

That's how enums are intended to behave. If you're not happy with it, maybe enums aren't really what you want to use. You could use plain objects and literal types instead. These behave similarly:

const MyMove = {
  Rock: 'X',
  Paper: 'Y',
  Scissors: 'Z',
} as const;
type MyMove = typeof MyMove[keyof typeof MyMove];
// type MyMove = "X" | "Y" | "Z"

const OpponentMove = {
  Rock: 'A',
  Paper: 'B',
  Scissors: 'C',
} as const;
type OpponentMove = typeof OpponentMove[keyof typeof OpponentMove];
// type OpponentMove = "A" | "B" | "C"  

but then either way of referring to the values will work since, for example, the type of MyMove.Rock is just the literal type "X" and not a special enum-flavored subtype of it:

const input: Array<[OpponentMove, MyMove]> = [
  ["A", "X"], ["B", "Y"]
]; // okay

const input: Array<[OpponentMove, MyMove]> = [
  [OpponentMove.Rock, MyMove.Rock],
  [OpponentMove.Paper, MyMove.Paper]
]; // still okay

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360