Reputation: 187144
Just found a critical bug in my typescript codebase due to the fact this was allowed:
const nums: number[] = { ...[1, 2, 3] } // should have been [ ...[1,2,3] ]
a.join('-')
// runtime error: a.join is not a function
Why is an array deconstructed as an object, assignable to array where it can cause an easily preventable runtime exception?
Upvotes: 3
Views: 165
Reputation: 329418
It's a design limitation of TypeScript; see microsoft/TypeScript#34780.
The type system doesn't have a way to mark interface
members as "own" or enumerable, so the compiler assumes that all members are copied via the spread operator. As a heuristic, that's often adequate, but it does the wrong thing for any members set on a prototype, like methods of a class:
interface Whoops {
foo(): void;
a: number;
b: string;
}
class Oops implements Whoops {
foo() { }
a = 1;
b = "";
}
const oopsie = (w: Whoops) => ({ ...w });
oopsie(new Oops()).foo(); // no compiler error
// runtime error: oopsie(...).foo is not a function!
If you write a class
declaration directly, the compiler will assume that method declarations are not spreadable:
declare class Whoops {
foo(): void;
a: number;
b: string;
}
const oopsie = (w: Whoops) => ({ ...w });
oopsie(new Whoops()).foo(); // compiler time error as expected
// foo does not exist on {a: number; b: string};
But unfortunately the type declarations for Array<T>
are for an interface
and not as a declared class
. So when you spread an array into an object the compiler thinks all of the Array
properties and methods are copied, and therefore that the resulting object conforms to the Array
interface, and therefore has a join()
method. Oopsie.
Maaaaybe someone could change the standard libraries so that instead of interface Array<T>
and interface ArrayConstructor
and declare var Array: ArrayConstructor
we just had declare class Array<T>
, and then join()
would no longer be seen as spreadable, but I'm not sure. It seemed to work when I tried it locally on my own system, but I can't easily reproduce this in the Playground or other online IDE, and messing with built-in types like Array
is not something I'm comfortable doing anyway.
Or maaaaybe the language could be changed so that non-own or non-enumerable properties could be marked on interface
s, but I wouldn't count on it (see microsoft/TypeScript#9726)
For now it's a design limitation of TypeScript. If you feel strongly about this you could go to microsoft/TypeScript#34780 and give it a 👍 and describe how you were bitten by it, but I don't know it'd really do much good.
Okay, hope that helps; good luck!
Upvotes: 3