Home > Back-end >  TypeScript: Change of a function argument type is not picked up in forEach within the function
TypeScript: Change of a function argument type is not picked up in forEach within the function

Time:09-02

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)
}

Here it is in a TS playground

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.

Playground link to code

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)
}
  • Related