Generic type extending union is not narrowed by type guard

Here’s why it doesn’t work: Typescript does control-flow type narrowing on regular variables, but not on type variables like your T. The type guard typeof x === "string" can be used to narrow the variable x to type string, but it cannot narrow T to be string, and does not try.

This makes sense, because T could be the union type string | number even when x is a string, so it would be unsound to narrow T itself, or to narrow T‘s upper bound. In theory it would be sound to narrow T to something like “a type which extends string | number but whose intersection with string is not never, but that would add an awful lot of complexity to the type system for relatively little gain. There is no fully general way around this except to use type assertions; for example, in your code, return 42 as Return<T>;.

That said, in your use-case you don’t need a generic function at all; you can just write two overload signatures:

// overload signatures
function typeSwitch(x: string): number;
function typeSwitch(x: number): string;
// implementation
function typeSwitch(x: string | number): string | number {
  if (typeof x === "string") {
    return 42;
  } else {
    // typeof x === "number" here
    return "Hello World!";
  }
}

Playground Link

Leave a Comment