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'.
}
}
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.
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
beforedata.filter
is finished?
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
}
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
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With