Home > Back-end >  Why does narrowing the type of a child property does not remove undefined/null from parent object�
Why does narrowing the type of a child property does not remove undefined/null from parent object�

Time:10-13

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!
}

Playground

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 || "";
}
  • Related