I want to know why this code behaves as it does, and how to fix it, if possible:
interface Optional {
opt?: string
}
function isNullOrUndefined<T>(val: T | null | undefined): val is null | undefined {
return val === null || val === undefined;
}
const x: Optional | undefined = Math.random() > 0.5 ? undefined : { opt: 'hoho' };
if (!isNullOrUndefined(x?.opt)) {
const y: string = x.opt // Error, even though it should be clear that x is defined
}
if (!isNullOrUndefined(x?.opt)) {
const y: string = x!.opt // No error, TS knows that child exists. So parent must also exist!
}
Similar questions have already been answered (unsatisfactorily) here and here. In both cases, the answer requires exact knowledge of the type to be checked, which is not helpful.
I suspect that this cannot be done better at the moment (TS 4.8.4). If so, is that a design decision or a shortcoming?
CodePudding user response:
It's because you need to narrow type of x:
Fix:
if (typeof x !== 'undefined' && !isNullOrUndefined(x.opt)) {
const y: string = x.opt
}
Breakdown of Fix:
if (typeof x !== 'undefined' && x.opt !== null && typeof x.opt !== 'undefined') {
const z: string = x.opt
}
On your second point:
if (!isNullOrUndefined(x?.opt)) {
const y: string = x!.opt // No error, TS knows that child exists. So parent must also exist!
}
You are using the ! operator which I believe tells TypeScript to trust you that it will be defined, which is effectively skipping undefined check. We only use that in special cases, I wouldn't use it here.
CodePudding user response:
Short answer: that's a design limitation.
You can read about this here and here.
You need to narrow the parent object also, narrowing parents based on properties is done only on special cases.
In your case a simple fix would be something like this:
if (!isNullOrUndefined(x) && !isNullOrUndefined(x?.opt)) {
const y: string = x.opt
}
Note: This -
if (!isNullOrUndefined(x?.opt)) { const y: string = x!.opt // No error, TS knows that child exists. So parent must also exist! }
You are just forcing TS
to believe that x
is not undefined. The type for foo
is checked via the type guard. That's the reason it works.
CodePudding user response:
There is no need to use generic parameter T
in the narrowing function (isNullOrUndefined
) which is a custom type guard. You want to pass a parameter with any
type to only guarantee that this type is null
or undefined
.
Therefore, this can be written more simply and solve the problem in this way:
function isNullOrUndefined(val: any): val is null | undefined {
return val === null || val === undefined;
}
Then x
type is Optional
in the if block:
if (!isNullOrUndefined(x)) {
const y: string = x.opt; //x type is Optional
}
But there is still a problem. You defined the opt
type as a union of string
and undefined
, and you want to assign it to y
which is a string
type. This causes an error and can be solved by doing one of these two things.
1- Remove opt
being optional
interface Optional {
opt: string
}
2- Put a runtime javascript checker condition that guarantees that if the value of opt
is undefined
, put an empty string in y
if (!isNullOrUndefined(x)) {
const y: string = x.opt || "";
}