Alex Wayne
Alex Wayne

Reputation: 187144

Why is this allowed? const nums: number[] = { ...[1, 2, 3] }

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 

Playground

Why is an array deconstructed as an object, assignable to array where it can cause an easily preventable runtime exception?

Upvotes: 3

Views: 165

Answers (1)

jcalz
jcalz

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 interfaces, 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!

Playground link to code

Upvotes: 3

Related Questions