Start The World's Best Introduction to TDD free...
while you still can!

Microtechniques

A Tale of map and parseInt in Three Acts

Act 1: What?!

import { describe, it, expect } from '@jest/globals';

describe("map parseInt over an array of strings", () => {
  it("should parse both numbers, but doesn't seem to", () => {
    expect(["0", "0"].map(parseInt)).toStrictEqual([0, 0]);
  });
});

The actual result is [0, NaN]. Huh.

Act 2: Hmmm….

import { describe, it, expect } from '@jest/globals';

describe("map parseInt over an array of strings", () => {
  it("should parse both numbers, but doesn't seem to", () => {
    expect(["0", "0"].map(s => parseInt(s))).toStrictEqual([0, 0]);
    expect(["0", "0"].map(parseInt)).toStrictEqual([0, NaN]);
  });
});

This test passes. Wait… when does eta-conversion fail?!

When there is more than one argument to the function we are “mapping”! What does Array.map() actually do?

callback is called with three arguments: the value of the element, the index of the element, and the object being traversed.

— https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.map

Act 3: Aha!

import { describe, it, expect } from '@jest/globals';

describe("map parseInt over an array of strings", () => {
  it("should parse both numbers, but doesn't seem to", () => {
    expect(["0", "0"].map(s => parseInt(s))).toStrictEqual([0, 0]);
    expect(["0", "0"].map(parseInt)).toStrictEqual([0, NaN]);
    expect(parseInt("0", 0, ["0", "0"])).toStrictEqual(0);
    expect(parseInt("0", 1, ["0", "0"])).toStrictEqual(NaN);    // because radix != 0 and radix < 2
  });
});

Since parseInt() interprets the index as a radix, and the radix is not 0 (where it would default to 10) and not 2 (which would result in interpreting the text as a binary number), the result is NaN. Obviously. (Read step 8.)

And that’s how we learn that eta-conversion with Array.map() in TypeScript is special.

Epilogue

import { describe, it, expect } from '@jest/globals';

describe("map parseInt over an array of strings", () => {
  it("should parse both numbers, but doesn't seem to", () => {
    // Explicit parameter passing works as expected
    expect(["0", "0"].map(s => parseInt(s))).toStrictEqual([0, 0]);

    // eta-conversion fails, because map() passes 3 arguments to the function, not only 1
    expect(["0", "0"].map(parseInt)).toStrictEqual([0, NaN]);

    // Notably, map() invokes callback(element, index, sourceObject),
    // but JavaScript/TypeScript can ignore superfluous arguments, which means...
    expect(parseInt("0", 0, ["0", "0"])).toStrictEqual(0);
    expect(parseInt("0", 1, ["0", "0"])).toStrictEqual(NaN);    // because radix != 0 and radix < 2
  });
});