-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inherited typing for class property initializers #10570
Comments
#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.) |
I'm actually okay with this idea - it reflects a lot of the same views we're taking with open/expando types. |
TODO: Try blindly taking the base class's type and see what happens in the RWC |
This 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 |
Nope, all properties |
How is that different from this summary on #6118, then? |
Make the declared type of the derived property the type of the base type's property, as if you had written |
I 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. @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. |
Problem ScopeThe only thing in scope is class property intializers, not method declarations. Problems SummaryWhen a base type (either an #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":
Contextual TypingWe very clearly need to be contextually typing initializer expressions with the type implied by the Computing Inferred Property TypesWhat 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)?
Option 1 is Terrible
Option 1 is what we do today. See above list Option 2 is Terrible
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 ChangeBreaksCurrently, 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
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 |
I'd vote Option 2 for |
A desired break not listed in the patterns above: https://stackoverflow.com/questions/44144178/typescript-doesnt-catch-state-type-errors-outside-of-constructor |
Actually it is not an error y['bleh'] = 10; // Error, but really shouldn't A bracket notation property access of the form var foo = y['bleh']; //expect foo to be of number type not any; |
Any update on where this is at? It's causing some very ugly casts just to get things working. |
currently 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 ! |
It 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 abstract class Base {
abstract x: Animal;
}
class Sub extends Base {
x = new Dog();
method() { this.x.woof(); }
} Potentially (as suggested earlier) the use of |
This issue has been open since 2016, any updates? |
Checking in on this a year after the last comment. |
Checking in after 8 years. |
|
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 & ...
whereB
is the base class andI1
,I2
,...
are theimplement
ed 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.B
expression is contextually typed by the unit-like typeE.A | E.B | E.C
and so acquires the specific typeE.B
rather than the intended typeE
! Daniel found this break in Azure./cc conspirators @DanielRosenwasser @sandersn
The text was updated successfully, but these errors were encountered: