- Notifications
You must be signed in to change notification settings - Fork 13k
Description
Problem
Initializing a class member with things like { }
, null
, undefined
, or []
has unexpected behavior.
class Base { favorites = ["red", "blue"]; } class Derived extends Base { favorites = []; constructor() { this.favorites.push('green'); // Can't push string onto never[], wat? } }
interface Settings { size?: number; color?: string; } class Base { settings: Settings = { size: 42 }; } class Derived extends Base { settings = { }; constructor() { if (big) this.settings = { siz: 100 }; // no error, wat? } }
Solution
New rule: When a class property is initialized with exactly null
, undefined
, { }
, or []
, the type of the property is taken from the same property of the inherited type (if one exists), rather than the type of the initializer.
The inherited type is B & I1 & I2 & ...
where B
is the base class and I1
, I2
, ...
are the implement
ed interfaces of the class.
Examples
interface Positionable { position: string | null; } class MyPos implements Positionable { position = null; setPos(x: string) { this.position = x; } getPos() { return this.position.subtr(3); // error detected } }
class Base { items = ['one']; } class Derived extends Base { items = []; // no longer an implicit any } var x = new Derived(); x.items.push(10); // Error as expected
Bad Ideas We Thought Were good
Contextual typing plays poorly with other behavior such as unit type positions. Consider
enum E { A, B, C } class Base { thing = E.A; } class Derived extends Base { thing = E.B; change() { this.thing = E.C; // Error! wat } }
This turns into a big problem because the E.B
expression is contextually typed by the unit-like type E.A | E.B | E.C
and so acquires the specific type E.B
rather than the intended type E
! Daniel found this break in Azure.
/cc conspirators @DanielRosenwasser @sandersn