Search code examples
typescriptnarrowing

Why doesn't TypeScript narrow array types?


Why is it that TypeScript doesn't narrow the type of arrays?

function test(input: (string | number)[]): string[] {
  // The type of .map(...) reports that it returns string[].
  input = input.map(x => x.toString())
  // Type error: Type '(string | number)[]' is not assignable to type 'string[]'.
  return input
}

The workaround is not depending on it narrowing the type by just immediately using or assigning to a fresh variable:

function test(input: (string | number)[]): string[] {
  return input.map(x => x.toString())
}

function test(input: (string | number)[]): string[] {
  const newInput = input.map(x => x.toString())
  return newInput
}

I did try casting, but in hindsight that obviously only works on use, e.g. return input as string[], and will not narrow the type, as .map(...) already returns the correctly narrowed type.

It feels counter intuitive to me having to do these workarounds. Why is it that TypeScript cannot narrow this array type and are there better workarounds available?

I did look into the official documentation and looked at similar questions on Stack Overflow, but unless I have overlooked something, I haven't seen this particular question answered with anything else than just to reassign.

It is what I am doing in my own code for now, but I just wish I knew why it is as it is and if I can do better.

> tsc --version                                                                                                                                                                                                                                                                              
Version 4.2.3

Solution

  • For better or worse, narrowing based on assignment or generally narrowing via control flow analysis (as implemented in microsoft/TypeScript#8010 only happens when the variables involved have union types. And by union type I mean where the type is itself directly a union, like {a: string} | {a: number} or Array<string> | Array<number>. A single object type with union-typed properties like {a: string | number} is not itself a union; nor is a generic interface specified with a union-typed type parameter like Array<string | number>. There is a longstanding suggestion at microsoft/TypeScript#16976 to support non-union control flow narrowing, but there's no indication when or if this will ever be implemented. So input = input.map(x => x.toString()) won't modify the apparent type of input.

    There are other narrowing type guards in TypeScript, such as the in operator, or the instanceof operator, and you can write your own user-defined type guard or an assertion function which can narrow the types of their inputs. None of these help you much here; by far the best workaround is just not to reuse the same variable to represent two different non-union types, as you know.