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.
CodePudding user response:
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.
CodePudding user response:
There is no point for a parameter to be optional if its type is boolean
and it is the only parameter in the function. Just use toggleCheckAll(true)
instead of toggleCheckAll()
. Plus, if you are declaring a function, use declare function
instead of declare const
declare function checkChecked(isAllChecked: boolean): void
const toggleCheckAll = (isAllChecked: boolean): void => {
checkBoxes.forEach(() => checkChecked(isAllChecked))
checkChecked(isAllChecked)
}
CodePudding user response:
The problem is that isAllChecked?: boolean
allows for undefined
, while isAllChecked: boolean
doesn't.
Either use a default value for the function, or add a check for undefined inside the function:
const toggleCheckAll = (isAllChecked: boolean = false): void => {
checkBoxes.forEach(() => checkChecked(isAllChecked))
checkChecked(isAllChecked)
}
Or
const toggleCheckAll = (isAllChecked?: boolean): void => {
if (isAllChecked == null) {
isAllChecked = false;
}
checkBoxes.forEach(() => checkChecked(isAllChecked))
checkChecked(isAllChecked)
}
EDIT
The second option doesn't work. See this other answer. Rather than introducing a new variable, I would suggest the Null Coalascing operator (??
):
const toggleCheckAll = (isAllChecked?: boolean): void => {
if (isAllChecked == null) {
isAllChecked = false;
}
checkBoxes.forEach(() => checkChecked(isAllChecked ?? false))
checkChecked(isAllChecked)
}