Repository navigation
Inherited typing for class property initializers #10570
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Aug 26, 2016 sandersn commented
on Aug 26, 2016 MemberMore actions#6118 is the first failed attempt, which has further discussion of the problems with contextual typing. (All 3 previous attempts were based on contextual typing.)
DanielRosenwasser commented
on Aug 28, 2016 MemberMore actionsI'm actually okay with this idea - it reflects a lot of the same views we're taking with open/expando types.
RyanCavanaugh commented
on Sep 19, 2016 MemberAuthorMore actionsTODO: Try blindly taking the base class's type and see what happens in the RWC
sandersn commented
on Sep 19, 2016 MemberMore actionsThis is basically what the summary of #6118 covers, except that it was for all properties, initialised or not. You intend to try with just properties that are initialised to
[],{},null,undefined, right?RyanCavanaugh commented
on Sep 19, 2016 MemberAuthorMore actionsNope, all properties
sandersn commented
on Sep 19, 2016 MemberMore actionsHow is that different from this summary on #6118, then?
RyanCavanaugh commented
on Sep 19, 2016 MemberAuthorMore actionsMake the declared type of the derived property the type of the base type's property, as if you had written
field: typeof super.field = expr), rather than contextually type expr withtypeof super.fieldsandersn commented
on Sep 20, 2016 MemberMore actionsI just realised I should have linked to #10610. Confusingly, my good/bad/ugly post in #6118 actually refers to an old version of that PR, which does basically what you describe, and as far as I recall, still hits some problem.
Daniel Rosenwasser (@DanielRosenwasser), you actually RWC with #10610. Do you have notes on the problem it hit? I remember it being literal-type related, so maybe it would be different now that we have literal types everywhere.
RyanCavanaugh commented
on Apr 25, 2017 MemberAuthorMore actionsProblem Scope
The only thing in scope is class property intializers, not method declarations.
Contextual typing of declared method parameters is interesting but a fully separable problem.Problems Summary
When a base type (either an
implementedinterface, or anextends'd base class) has a property with the same name, our current behavior when a derived class initialized that type is not good, leading to frequent user confusion.#10484, #11714, #15191: Initialize union type from string literal
interface I { xOrY: 'x' | 'y' } // Error, cannot assign "string" to "'x' | 'y'"" class C implements I { xOrY = 'x' }
#1373, #15136: Initialize function-typed field with function expression with unannotated parameter
class Class<T> { a: (b: T) => string } class Class2 extends Class<number> { a = b => 'b' // Error, Parameter 'b' implicitly has an 'any' type. }
#14684: Implicit any error when initializing with empty array
// (noImplicitAny ON, strictNullChecks OFF) interface I { obj: { x: string[] } } // Error: x class C implements I { obj = { x: [] } }
Optional properties vanish
interface HasOpts { opts: { x: number; y?: number; } } class Foo implements HasOpts { opts = { x: 3 }; bar() { // Error, property 'y' is excess this.opts = { x: 3, y: 6 }; } }
In approximate order of severity based on "hits":
- Initialization of a string-literal-typed field using a string always fails
- Initialization of a function-typed field with a function expression doesn't un-implicit-any the parameters (and produces an
anyparameter) - Initialization of an array-typed field with an empty array triggers an implicit any (and produces an
anyfield) - Initialization of an object-typed field with an object literal causes unspecified optional properties to disappear from the class property type
Contextual Typing
We very clearly need to be contextually typing initializer expressions with the type implied by the
extendsorimplementsclauses.
This is the only plausible way we can fix parameters of function expressions.Computing Inferred Property Types
What should the type of an initialized property with a matching property in the base type be (when there is no type annotation in the derived class)?
Three appealing options:- The type of the expression
- The type of the expression as contextually typed by the base type property
- The type of the base type property
Option 1 is Terrible
- The type of the expression
Option 1 is what we do today. See above list
Option 2 is Terrible
- The type of the expression as contextually typed by the base type property
We have a long-standing tenet that the type of a contextually-typed expression should not be observable. This currently shows up in assignment expressions:
type Obj = { [n: string]: number } // OK var x: Obj = { baz: 100 }; x['bleh'] = 10; var y = x = { baz: 100 }; y['bleh'] = 10; // Error, but really shouldn't
and
var x: string[] = []; // x: string[] var y = x; // y: string[] var z = x = []; // z: implicit any[]
Simply using the contextually-typed type of the initializing expression also gives what appears to be "wrong" behavior for union types:
interface I { xOrY: 'x' | 'y' } class C implements I { // Literals contextually typed by literal unions don't widen xOrY = 'x'; // xOrY: 'x' changeUp() { this.xOrY = 'y'; // Error, cannot convert 'y' to 'x' } }
Option 3 is a Breaking Change
Breaks
Currently, this code is OK:
class Animal { parent = new Animal(); move() { } } class Dog extends Animal { parent = new Dog(); woof() { this.parent.woof(); } }
If we simply "copy down" the type of
Animal.parent, thenthis.parent.woof()will be invalid.
The user will have to write a type annotation instead:parent: Dog = new Dog();Of course, this code is already suspicious because it's unsound to writes through base class aliases:
class Animal { parent = new Animal(); move() { } setParent(c: Cat) { this.parent = c; } }
but in general we are not strict about this kind of thing, and in practice this is a difficult "error" to make.
Suspicious Changes?
This code is the same as an earlier example, but the spelling has been changed:
interface I { kind: 'widget' | 'gadget' } class C implements I { kind = 'widget'; }
It would appear that
kindshould be of type'widget', not'widget' | 'gadget'.
Of course, today its type isstring(and a compile error), but it's still perhaps unexpected.Reacted by Herrington Darkholme, Eric, Jacob Stamm and Michał SkowronekI'd vote Option 2 for
readonlyproperties and Option 3 for mutable ones.Reacted by NoahRyanCavanaugh commented
on May 23, 2017 MemberAuthorMore actionsA desired break not listed in the patterns above: https://stackoverflow.com/questions/44144178/typescript-doesnt-catch-state-type-errors-outside-of-constructor
Reacted by Rick CarlinoActually it is not an error
y['bleh'] = 10; // Error, but really shouldn't
A bracket notation property access of the form
object [ index ]
where object and index are expressions, is used to access the property with the name computed by the index expression on the given object. A bracket notation property access is processed as follows at compile-time:
...
Otherwise, if index is of type Any, the String or Number primitive type, or an enum type, the property access is of type Any.
spec
I think you meant this:var foo = y['bleh']; //expect foo to be of number type not any;
1 remaining item
It is a similar issue
function d(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<(x: number) => void>) { } class A { @d f(x) { }//x is any }
Reacted by Boris BermanThis is another common case for me:
import * as React from 'react'; interface State { foo?: number } class A extends React.Component<{}, State> { state = { } render() { return <span>{this.state.foo}</span>; // ERROR, since property foo doesn't exist on this.state } }Instead I usually have to use the constructor to do this style of initialization, wish I wouldn't have to tell the engineers I'm working with to do that. This is a contrived example but you get the idea.
Reacted by markron, Eric, Veniamin Krol, Kristóf Poduszló, Ilya Semenov and Maciej HolyszkoIs there any update on this issue? It makes the usage of this codegen implementation very cumbersome.
Is there any update on this issue? I am eager to see it in the closest releases.
Regarding this feedback:
Of course, there may be some breaking changes, but those will appear in bad code (with implicit
any) and some of them can be fixed, like setting the type of the property by its initializer if it is assignable to the type from base/implemented interface. I think that this feature will bring more advantages (like less boilerplate and narrowing the sources of truth throughout the types of TypeScript apps).Reacted by Katja Lutz, Omar Diab, Ian MacLeod, Vitalii Kryvenko, Lizzi Lindboe, Lewis Horwood, jamie-pate, Anthony Lenglet, Maciej Holyszko and Walker GrayAnthonyLenglet commented
on Oct 15, 2019 More actionscurrently we're fixing this by doing:
interface IStore { id: string; storeCode: number; } export class Store implements IStore { public id: IStore['id'] = null; public storeCode: IStore['storeCode'] = null; constructor(item: IStore) { Object.assign(this, item, {}); } }
this works but a fix would definitely be really cool to get here !
Reacted by James Irwin, Nick Pelone, Sviatoslav, Noah and Mario JerkovicReacted by Steve Richardson, Andrew and BlackGloryIt seems like #6118 had a mostly reasonable solution, except that it handled multiple inheritance via interfaces incorrectly. Under "The Bad" it listed an example like
abstract class Base { abstract x: number|string; } interface Contract { x: number; } class Sub extends Base implements Contract { x = 42; }
as failing because the type of
xwas taken fromBase['x']only, rather than(Base & Contract)['x'], which would have given the correct contextual type. This still does not address breakages when subtypes have a narrowed "kind":abstract class Base { abstract x: Animal; } class Sub extends Base { x = new Dog(); method() { this.x.woof(); } }
Potentially (as suggested earlier) the use of
readonly(and/oras const?) could affect the contextualized type sufficiently to at least make fixes trivial? In the above example, ifBase['x']werereadonlythen presumably it's safe(r) to inferSub['x']asDog? (Conversely, it's not actually an error but probably should be for onlySub['x']to bereadonly- the same thing might be fine there).Reacted by Maciej HolyszkoSame here by using ts with jsdoc in js for get ref, Intelisence is broke and lint type error !
With the arrival of jsdoc and ts support, I hoped to be able to stay in js vanilla with full types support !

4yr old issue !, i suppose i will need search another architecture approach ! 😢
pity, that restricts some ways to easily architect our projects ! hope a good ts guy can fix this someday !Reacted by Noahok actually this seem a good approach for me, but need using static for avoid import.

it's not too ugly and remains readable and accessible.
But I'd rather want do without static and force types in the children class !const TypesEnum = { abc: 'abc', abcd: 'abcd', abcde: 'abcde', }; export class Base { static TypesEnum = TypesEnum /**@type {keyof TypesEnum} - all work fine here, default null for the example purpose */ _test = null; constructor(data) {} } export class A extends Base { /** @type {Base['_test']} */ _test = 'abc'; //lint a error and we lost intellisense in Vscode , need force @type! constructor(data) { super(); } }
Problem
Initializing a class member with things like
{ },null,undefined, or[]has unexpected behavior.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 & ...whereBis the base class andI1,I2,...are theimplemented interfaces of the class.Examples
Bad Ideas We Thought Were good
Contextual typing plays poorly with other behavior such as unit type positions. Consider
This turns into a big problem because the
E.Bexpression is contextually typed by the unit-like typeE.A | E.B | E.Cand so acquires the specific typeE.Brather than the intended typeE! Daniel found this break in Azure./cc conspirators Daniel Rosenwasser (@DanielRosenwasser) Nathan Shively-Sanders (@sandersn)