typescripttype-narrowing

type narrowing doesn't work when using a non-type-literal field as a tag


Inside the test function, b type does not narrow to string when a type is string.
Why? I don't understand why it behaves this way.

type Foo = {
  a: string;
  b: string;
};
type Bar = {
  a: number;
  b: number;
};

type Uinon = Foo | Bar;

function test({ a, b }: Uinon) {
  if (typeof a === "string") {
    a;
    b;
  }
}

playground

If a is type literal as shown below, this is fine, but if it is not type literal, why is it not type narrowing?

type Foo = {
  a: "A";
  b: string;
};
type Bar = {
  a: "B";
  b: number;
};

Solution

  • The feature you are using is a special case of type narrowing, called a discriminated union. From the TypeScript Handbook v1:

    Discriminating Unions

    A common technique for working with unions is to have a single field which uses literal types which you can use to let TypeScript narrow down the possible current type.

    And this discrimination is only implemented for literal types. The new handbook doesn't state it so explicitly as a rule, but also only uses literal types.

    You can still implement your own type predicate to narrow down your Union:

    function isFoo(u: Union): u is Foo {
      return typeof u.a === "string"
    }
    
    function test(u: Union) {
      if (isFoo(u)) {
        u.a;
        u.b;
      }
    }