Issue
I am working on something that has checkboxes and I don't understand why the following is not valid TypeScript:
declare function checkChecked(isAllChecked: boolean): void
declare function determineIfChecked(): boolean
const checkBoxes: number[] = []
const toggleCheckAll = (isAllChecked?: boolean): void => {
if (isAllChecked === undefined) isAllChecked = determineIfChecked()
checkBoxes.forEach(() => checkChecked(isAllChecked))
checkChecked(isAllChecked)
}
The isAllChecked
in the call of checkChecked
in the forEach
loop raises an:
Argument of type 'boolean | undefined' is not assignable to parameter of type 'boolean'. Type 'undefined' is not assignable to type 'boolean'.
I'm guessing my understanding of the scope of function arguments is not quite right as I would have thought the if statement should have updated the type of isAllChecked
to be boolean
and that this would be carried through to the forEach
loop.
I can work round this but I would like to understand why this is not valid TypeScript.
Solution
This is a well-known design limitation in TypeScript; see microsoft/TypeScript#9998 for a full description.
The problem is that the narrowing that happens when you check and reassign isAllChecked
does not persist across function scope boundaries. So while the compiler knows that isAllChecked
is definitely boolean
in the outer scope, that information is completely gone in the scope of the callback () => checkChecked(isAllChecked)
. Any narrowing of closed-over values is reset inside a closure.
This happens because in general it would be completely unfeasible for the compiler to attempt to keep track of control flow into and out of functions. The compiler doesn't know when or if the callback () => checkChecked(isAllChecked)
will be run, because even though we know that the array forEach()
method runs essentially synchronously, that information is not part of the type system. In general, a callback passed to some function could be called at any time.
And in general, closed-over values could be reassigned or modified before that time. In your particular case, you never modify isAllChecked
after the callback is created. But the compiler would need to spend extra resources to check that this is true, and therefore all closures would require the compiler to check for reassignments of all closed-over values everywhere, which would add a lot of compilation time for a benefit that only happens on occasion.
So that's what's going on. The compiler can't keep track of what happens to closed-over values inside of callbacks, and it doesn't really try.
I know you're not asking for a workaround, but for completeness, the usual workaround in cases like this is to copy the value to a const
once you know it will never change:
const toggleCheckAll = (isAllChecked?: boolean): void => {
if (isAllChecked === undefined) isAllChecked = false;
const i = isAllChecked; // boolean
checkBoxes.forEach(() => checkChecked(i)) // okay
checkChecked(isAllChecked)
}
The i
constant has type boolean
and not boolean | undefined
because of the control flow narrowing of isAllChecked
to boolean
at the time of the assignment to i
. Which means that checkChecked(i)
is always acceptable no matter where it appears; there is no control flow narrowing of i
to be reset.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.