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

3

12

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'.
    }
}
Thermy answered 14/7, 2021 at 3:56 Comment(0)
C
9

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
    }
}
Comber answered 14/7, 2021 at 6:38 Comment(0)
A
3

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

Amorete answered 14/7, 2021 at 5:49 Comment(2)
Quite correct. It's probably better to just pass a bound reference though instead of using ! though. – Lukewarm
@AluanHaddad In this particular example, using ! is what you should not do because it leads to the invocation of an undefined object. – Amorete
E
-1

it's in the last line, you need to conditionally handle cases in which obj.func is undefined for v to be passed to it:

the longhand type of func

func?: ((str: string) => boolean) | undefined

using a ternary to conditionally handle undefined satisfies this:

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 ? obj.func(v) : console.log('obj.func undefined')) // ng Cannot invoke an object which is possibly 'undefined'.
    }
}
Exorcist answered 14/7, 2021 at 4:20 Comment(3)
You're missing the point of the question: The asker believes the if(obj.func !== undefined) should narrow the type of obj.func to (str: string) => boolean). Notice the line above it is "ok". So why doesn't the narrowing apply on the last line? I know the answer and will write it up in a bit. – Comber
Because you can't invoke an object which is potentially undefined. It's undefined because obj.func !==undefined is not of the same type as the invoked form of obj.func. That's why its inert form is okay for filtering, whereas its invoked form requires subsequent checks for undefined. Additionally, it's passed in as an argument which is of type unknown according to the typedef for the filter method. – Exorcist
Incorrect. As you can see proven in my answer, it has nothing to do with "inert" vs "invoked", nor anything to do with "typedef for the filter method". – Comber

© 2022 - 2024 β€” McMap. All rights reserved.