Typescript not omitting types properly

Viewed 76

I'm trying to make a function which takes either a number or the string 'last' and uses corresponding element from an array.

I handle the case where input is 'last'.

I've added a simplified example. TS Playground

I'm getting an error on the return state[i] line.

I tried moving dispatch... to an if block with (typeof i === 'number') but that doesn't work either.

export function moveTo(i: number | 'last') {
  return (dispatch: Function, getState: ()=>string[]) => {
    const state = getState();
    if (i === 'last') {
      i = state.length - 1;
    }
    dispatch({
      index: i, //here i is shown to be number on hover
      effect() {
        return state[i]; // index expression is not of type number (shows as number | 'last')
      }
    });
  };
}

I want to assert after the if statement that i is number so that I don't have to use as number everywhere in the effect function.

2 Answers

Since i can be a string and a number the linter struggle to understand the conversion. One solution is to use another variable:

export function moveTo(i: number | 'last') {
  return (dispatch: Function, getState: ()=>string[]) => {
    const state = getState();
    const index: number = (i == 'last') ? state.length - 1 : i;

    dispatch({
      index,
      effect() {
        return state[index];
      }
    });
  };
}

Also, it's a bad thing to have multiple types for the same variable (as things like that can occur), I suggest using -1 instead of 'last' to keep with the number type.

The type for i in the effect callback is not narrowed, because i is considered a mutable parameter of function moveTo.

That means, when effect callback is invoked sometime later, the compiler cannot be sure that i still has a number type. As a consequence, the type guard induced by if (i === 'last') {i = state.length - 1;} is invalidated, and i gets the initial number | 'last' type in the callback.

So when does TS consider function parameters as const?

Function parameters are implicitly treated as const, until you make an assignment to the parameter somewhere in the function or a nested function. When a parameter is an implicit const, its narrowed type is still accessible in nested function expressions and arrow functions via control flow analysis/type guards. So, this contrived example works:

export function moveTo(i: number | 'last') {
  return (dispatch: Function, getState: () => string[]) => {
    if (i !== 'last')
      dispatch({
        index: i,
        effect() {
          return getState()[i];
        }
      })
  };
}

And what to do now?

Your best bet probably is to declare a new const variable (see also @D. Nathanael's answer), which produces more consistent types than as number casts in the callback. Airbnb JavaScript Style Guide does not recommend function parameter re-assignments, so you could see that as chance to clean up the code a bit by treating i as immutable.

Related