class C {
x: number;
y: number;
constructor() {
this.x = 0;
}
}
new C().y.toString(); // should produce error
I know this is complicated (what about derived classes? what if there is this.initializeY()?) but I don't think I've seen it being discussed on the issue tracker so far...
Some existing research that might help:
"Declaring and Checking Non-Null Types in an Object-Oriented Language", Manuel F盲hndrich, K. Rustan M. Leino (2003). Link to PDF.
Basic idea: The type _Traw_ means "the same as type _T_, except the field types are all union'd with null". While an object is still being initialized (e.g. in the constructor), the type of this is _Traw_, so everyone using the object has to be aware that the fields might be null. Once all of the fields are initialized, this has type "T".
"Masked Types for Sound Object Initialization", Xin Qi, Andrew C. Meyers (2009). Link to PDF.
A more elaborate/precise solution. Basic idea: The type _T\f\g!\h_ means "the same as type _T_ except the fields _f_ and _h_ might not be initialized yet and the field _g_ definitely isn't initialized yet."
Also, function types can have variable initialization effects, so you can have initialize some fields in a helper function instead of having to do all initialization in the constructor.
+1 for this. It has caught me quite a few times. TBH, without this, it makes class member variables quite brittle.
I'm suprised that this issue has been brought up 4-5 times but there's zero update from the team. Is this a feature that's not going to be supported or it's actually on the roadmap?
I am wondering if there could be a flag to enforce assignment in the constructor? I suppose this need is very much based on my use of classes, but it seems to me that if a class variable
class A {
t: number;
constructor() {
}
}
is not assigned in the constructor, the contract of t: number does not hold?
Yeah, it's a bit worrisome that this is still open after two years. It's a glaring omission that can really have consequences for your app...
I think the reason why it hasn't been implemented (even now) is that a class member might gets initialized at a later point. I think that is what @cakoose 's summarize comes down to. So if one just performs initialization checks for the constructor you would need to declare all members which get initialized later (but eventually) as Maybe. But then you have to do null checks or non-null assertions all over the place, although that member shouldn't be conceived as ever be null.
In TypeScript, with strict initialization checks on, you are forced to declare such a member as Maybe, and then, because you know (or at least think) it isn't null, you just use the non-null-assertion operator "!" everywhere and lose all guarantees anyway. So it is quite useless, I think, one can easily see why they need such an operator (which should just not exist!).
In the end a class author has to know which members get initialized, where they get initialized and if it is safe to do that later.
Maybe with clever annotation or special comments one could signal to flow that a member gets initialized in another function, just not in the constructor. And than Flow might be able to do some clever flow analysis from there. But I think that is rather unrealistic, because flow cannot know how a class is supposed to be used.
@MarvinHannot The problem is that this issue compromises the soundness of the whole type system. The non-null guarantee is violated for all member variables. It would be far better if all member variables were implicitly nullable, given that. At least give me the option to be pedantic.
I must explicitly check member variables for null, because they might not be initialized. That's just how it is, because that's just how JavaScript is. I don't want Flow to sweep this language deficiency under the rug. I want it to save me from it.
If a member var does not have a default value, and isn't initialized in the constructor, then by definition it is nullable.
I can and should be forced to use refinement or cast to inspect such a value, because I can and will sometimes observe it to be null.
A member variable cannot be "initialized" outside the constructor. Anything not assigned to in the constructor, or given a default value, is implicitly undefined, a.k.a. nullable.
There are two acceptable solutions:
Treating every member var as nullable regardless is in no way necessary. Neither are you "forced" to ! assert non-null! But as it stands, you should be forced to null-check member variables.
@MarvinHannot I will add, within the constructor, all member variables not given defaults are implicitly nullable. But by definition, any member var assigned to within the constructor will never be obvserved as uninitialized from outside the constructor.
The problem is that this issue compromises the soundness of the whole type system. The non-null guarantee is violated for all member variables. It would be far better if all member variables were implicitly nullable, given that. At least give me the option to be pedantic.
If a member var does not have a default value, and isn't initialized in the constructor, then by definition it is nullable.
Well, nullable means that you could assign null (or undefined) to a member, which you can't when it isn't marked as such. So it really comes down to initialization, which doesn't has to be done in the constructor. It could even be done by an exported function while the class itself remains hidden from the user. Or it could be done by a "friend" class in the same module. In either case, marking members as Maybe just doesn't describe the interface in an accurate way or leads to unnecessary null checks. and I will give you a simple example why defensive null checks aren't good enough. Think of a LinkedList with a head and tail:
class LinkedList {
#head: ?Node
#tail: Node
add(value: any) {
if (head === undefined) {
this.#head = new Node(value)
this.#tail = this.#head
return
}
...
}
}
We know for certain, if the head is initialized, the tail will be as well. But if the tail were a Maybe too we would have to do another null check for that or just an any assertion, which makes the whole thing silly. And we can't give tail any reasonable default value, nor would it make any difference.
Another simple (yet more constructed) example:
class A {
#switch = false
#member1: number
#member2: number
static initSwitch(){
this.#switch = true
}
getValue() {
if (switch) {
return this.#member1
}
return this.#member2
}
}
These kind of "switches" are rather common. We know for certain which member got initialized and is safe to use without further null checks. But the compiler doesn't have any way to check that assertion.
And there are enough further examples where you know that a member got initialized after construction and is inherently non-nullable.
"Initialization after construction" is an anti-pattern that should be linted. That people do it all the time is not a justification for allowing it. I am using Flow because I want soundness.
Well, nullable means that you could assign null (or undefined) to a member, which you can't when it isn't marked as such. So it really comes down to initialization, which doesn't has to be done in the constructor. It could even be done by an exported function while the class itself remains hidden from the user. Or it could be done by a "friend" class in the same module. In either case, marking members as Maybe just doesn't describe the interface in an accurate way or leads to unnecessary null checks. and I will give you a simple example why defensive null checks aren't good enough. Think of a LinkedList with a head and tail:
I think what you're trying to say is that from the point of view of assignment, a plain member variable is non-nullable, and I agree with that. But your observation hints at the solution.
Also, your focus on methods is obscuring the real problem. The real problem is that member variables are nullable for _users_ of your class. Consider this:
```
class Value {v: any}
class SomeClass {
value: Value
callMeFirstOrElse() {
this.value = new Value("foo");
}
}
let sc = new SomeClass();
console.log(sc.value.v); // oops, sc.value is null!
```
The last line throws, despite passes type checking. I argue flow should consider sc.value nullable, because it's not guaranteed to be non-null. It's a real common problem. It has become one of the few ways my type-checked code can still blow up at runtime.
TypeScript's solution might be heavy-handed, but at least it's correct.
Flow could reach a compromise by adding some special rules that apply only within constructor bodies. Constructors do have a special status, so this doesn't seem all that onerous.
super()this can not escape the constructor.this as an argument), but it's a separate issue.Constructors really do have this special view of the object under construction, so these rules seem pretty intuitive as a JavaScript rogrammer. How reasonable the implementation would be is a separate issue. Maybe there's an argument to be made for a new type, that means "might not be initialized".
Most helpful comment
I'm suprised that this issue has been brought up 4-5 times but there's zero update from the team. Is this a feature that's not going to be supported or it's actually on the roadmap?