Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Cannot invoke an object which is possibly 'undefined'" even after ensuring it !== undefined

Why do I get a Cannot invoke an object which is possibly 'undefined' Typescript error even after I check that the func reference is not undefined?

type Hoge = {
    func?: (str: string) => boolean
}

const myFunc = (obj: Hoge) => {
    const data = ['AAA', 'BBB', 'CCC']

    if(obj.func !== undefined) {
        data.filter(obj.func) // ok
        data.filter(v => obj.func(v)) // ng Cannot invoke an object which is possibly 'undefined'.
    }
}
like image 621
green Avatar asked Jul 14 '21 03:07

green


Video Answer


2 Answers

short answer

Control flow analysis is complicated, and Typescript's analysis only goes so far. In this case, it easily proves that on the //ok line, data.func !== undefined. But it is not so easy to prove data.func's value will not change before it is invoked sometime in the future within the closure that is passed to data.filter.

See the solution at the end of this answer.

long answer

Type narrowing is achieved by using control flow analysis to prove that a reference on a particular line has a narrower type than its originally declared or previously known type.

For the line

data.filter(obj.func) // ok

the control flow analysis is trivial; obj.func is dereferenced immediately after it was checked to be !== undefined.

But in the next line

data.filter(v => obj.func(v))

obj.func is NOT dereferenced immediately. It only appears on the next line lexically. But in fact, it won't be invoked until later, "inside" the execution of data.filter. Typescript would have to recursively do control flow analysis down into the implementation of data.filter. Obviously it does not in this case. Maybe a future version of Typescript will (they keep improving it). Or maybe it's too complex or expensive. Or maybe it's impossible?

🟣 Help me improve this answer

Does Javascript's "single threaded architecture" mean that no other thread can change the value of obj.func before data.filter is finished?

see for yourself

Put the following code in your IDE or try it in the Typescript Playground. Observe the types of a, b, c, d and e. Notice how c, which lexically appears between b and d, has a different type. This is because wrappingFunc does not actually execute between b and d. The type of c cannot be not narrowed simply because it appears lexically within the if clause. Notice how the value of obj.func is modified before wrappingFunc is called:

type Hoge = {
    func?: (str: string) => boolean
}

const myFunc = (obj: Hoge) => {
    const data = ['AAA', 'BBB', 'CCC']

    const a = obj.func           // ((str: string) => boolean) | undefined

    if(obj.func !== undefined) {
        const b = obj.func       // (str: string) => boolean

        const wrappingFunc = function () {
            const c = obj.func   // ((str: string) => boolean) | undefined
            c()                  // ERROR
        }

        const d = obj.func       // (str: string) => boolean

        obj.func = undefined     // modify obj.func before calling wrappingFunc
        wrappingFunc()           // this call will fail; Typescript catches this possibility above
   }

    const e = obj.func           // ((str: string) => boolean) | undefined
}

solution

One way to fix the error is to use a type assertion, which is basically saying to Typescript: "You may not know the type, but I do, so trust me.":

const myFunc = (obj: Hoge) => {
    const data = ['AAA', 'BBB', 'CCC']

    if(obj.func !== undefined) {
        data.filter(obj.func) // ok
        data.filter(v => (obj.func as (str: string) => boolean)(v) ) 
    }
}

Another way is to assign the value of obj.func to a variable in the closure that Typescript can easily prove is never modified:

const myFunc = (obj: Hoge) => {
    const data = ['AAA', 'BBB', 'CCC']

    if(obj.func !== undefined) {
        data.filter(obj.func) // ok
        const filterFunc = obj.func
        data.filter(v => filterFunc(v)) // ok
    }
}
like image 154
Inigo Avatar answered Oct 21 '22 17:10

Inigo


Well you know that the callback to data.filter is executed immediately but how is typescript supposed to know that?

Consider this example:

type Hoge = {
    func?: (str: string) => boolean
}

const myFunc = (obj: Hoge) => {
    if(obj.func !== undefined) {
        setTimeout(() => obj.func!('a'), 100); // Force assume func is not undefined
    }
    obj.func = undefined; // TS will allow this since func can be undefined, but this is a problem
}
myFunc({
    func: (str: string) => { 
        console.log(str); return true; 
    }
})

Since in your case you do know the function is called within the if block you should use:

data.filter(v => obj.func!(v))

This would let TS know that you know that the function is not undefined at that point

Playground link

like image 34
apokryfos Avatar answered Oct 21 '22 18:10

apokryfos