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
}
}
!
though. β Lukewarm