TypeScript Version: 2.2.1 / nightly (2.2.0-dev.201xxxxx)
Code
class mayBeNull {
importantValue: number | null = null;
check(): this is checkedCannotBeNull {
return this !== null;
}
}
interface checkedCannotBeNull {
importantValue: number;
}
const obj = new mayBeNull;
obj.importantValue; // "number | null", which is intented
if (obj.check()) {
obj.importantValue; // Here TypeScript reports "number | (null & number)"
}
Expected behavior:
Once a.check() returns true, obj has to be both checkedCannotBeNull and mayBeNull. Hence, obj.number cannot be null because otherwise it will not fulfill interface checkedCannotBeNull.
This is working as intended. When a type guard has a target type that isn't a subtype of the source type, TypeScript produces an intersection type. In your example, checkedCannotBeNull is not a subtype of mayBeNull because it is missing the check method. So, under the type guard obj has type mayBeNull & checkedCannotBeNull and therefore the importantValue member has type number | (null & number). It works if you change your declaration to
interface checkedCannotBeNull {
importantValue: number;
check(): this is checkedCannotBeNull;
}
What does null & number do? Shouldn't it be computed as never?
Shouldn't it be computed as never?
generally speaking please no, for example number & T is a valid use case for, pardon me, so-called tagged types, it can be used as a number ONLY WHERE it's accompanied by a tag: #4895
@ahejlsberg
Yes, I do agree that obj should be mayBeNull & checkedCannotBeNull. However, isn't that importantValue should have type number & (null | number) and thus evaluates to number?
Say we have a class Old for age >= 2 and a class Young for age <= 2. If someone is classified as (Old & Young), shouldn't his age be ((>=2) & (<=2)) instead of ((>=2) | (<=2))?
However, isn't that importantValue should have type number & (null | number)
@SCLeoX It should, but it's same because number & (null | number) = (number & null) | (number & number) = (number & null) | number. TS computes A & (B | C | D) as (A & B) | (A & C) | (A & D) which is actually harder to read...
@SaschaNaz I see what you mean (hopefully). However, (number & null) in (number & null) | (number & number) is just never. Never | (number & number) should be number instead of number | null.
I agree, that's what I said on the second comment 馃槃
This issue seems to have been already addressed or is unactionable at the moment. I am auto-closing it for now.
@mhegazy I think the person "addressed" this issue misunderstand my point...
@SCLeoX The core issue here is number & null is not being never and probably you will want to re-post this issue with more proper title.
Intersection types allow creating empty types, but they do not all resolve to never. this is the intended design.
@mhegazy Can you explain a bit more about the intention?
var a: any;
if (typeof a === "number" && typeof a === "string") {
a // number & string cannot occur so is never
}
var b: number & string; // ??
the narrowing with typeof here does something special and it is not the rule in general. i would argue that should change.
for intersection, it is meant to be simple and consistent, T & U, is always T & U and not iff the set of values of T&U is not empty. that could result in types that are empty, but that is allowed. a use case of these are tagged primitive types so number & "tag" for instance.
Hmm, I would post about the inconsistency then. (Not really convinced, however... The tagged type use case looks too hacky :/)
@mhegazy Thank you for explaining this. I understand that number & null cannot be just evaluated to never. However, something that is both number and null must get the essence of number in it. I should still be able to use it as a number because it is just a number with additional properties of null. Hence, in my example, even if the property's type is evaluated to be number | (null & number), I should still be able to use it as a number.
However, something that is both number and null must get the essence of number in it. I should still be able to use it as a number because it is just a number with additional properties of null.
the same argument goes both ways. it also has the essence of null in it.
The main issue here is that there is no way to know that intended to remove mayBeNull all together
or just narrow it to a more specialized type. @ahejlsberg's suggestion above should help.
Most helpful comment
I agree, that's what I said on the second comment 馃槃