Typescript lets you spread an array into an object
I ran into an instance of this Typescript bug recently. Basically, Typescript lets you do this without compiler errors:
const arr = [1, 2, 3];
const x: { a: number[] } = { a: { ...arr } };
x.a.map(n => n + 1);
Note the { ...arr }
instead of [...arr]
! Running this throws the error x.a.map is not a function
. Because objects don't have .map
.
The problem here is that Javascript is sorta objecty, and arrays are objects. So spreading the array arr
into a property that should be an array obj.a
by doing obj.a = { ...arr }
looks correct to Typescript, because arr
has all the properties of an array.
However, Javascript is also a mess. Object.assign
(which is what the spread is using) does Special Stuff. Properties in Javascript have additional "qualities". Object.assign
only spreads out those properties that are "own" and "enumerable". None of those fancy array properties (like .map
) are actually own or enumerable.
Doing obj.a = { ...arr }
is a real Javascript thing that actually works, and spreads out the indices to the values, so obj.a["0"]
will be the first value of the array. Those are the own enumerable properties of a Javascript array:
console.log(Object.entries(x.a));
// [["0", 1], ["1", 2], ["2", 3]]
Typescript doesn't track own-ness or enumerable-ness of properties, and so the Typescript compiler cannot actually know that .map
isn't spread into the object when you do { ...arr }
.
If you've run into this and are seeing this, you're not alone! This is being tracked in typescript-eslint
. At time of writing there's an open PR to add a rule preventing this.
Additionally this is a great expansion of Predictable programming 1: how Typescript isn't Rust and generally a supporting argument for predictable programming.