简体   繁体   中英

Typescript: Discriminated union based on type

Why is the following not possible for TS ? Why cant I use a type as discriminant ?

export interface A1 {
  plop: number;
}
export interface B1 {
  hop: number;
}

export interface A {
  foo: number;
  bar: string;
  inner: A1;
}

export interface B {
  foo: number;
  bar: string;
  inner: B1;
}

export type AorB = A | B;

function test(): AorB {
  let inner: A1 | B1;

  if (Math.random()) {
    inner = {plop: 4};
  } else {
    inner = {hop: 43};
  }

  return {
    foo: 42,
    bar: 'plop',
    inner
  };
}

The TS compiler tells me :

  Type '{ foo: number; bar: string; inner: A1 | B1; }' is not assignable to type 'B'.
    Types of property 'inner' are incompatible.
      Type 'A1 | B1' is not assignable to type 'B1'.
        Property 'hop' is missing in type 'A1' but required in type 'B1'.

Consider for a moment the expansion of your types. The type AorB represents the union:

{
  foo: number;
  bar: string;
  inner: A1;
} | {
  foo: number;
  bar: string;
  inner: B1;
}

Noting that nowhere in this does the type A1 | B1 A1 | B1 appear. This is to say that the type AorB is expecting to have an object where the inner property is known and fixed as either A1 or B1 .

But wait? Since the properties of the enclosing object are the same (namely foo and bar ), shouldn't the type above be equivalent to:

{
  foo: number;
  bar: string;
  inner: A1 | B1;
}

Logically, that makes sense, you can distribute the inner union over the enclosing object type and see that you would get the same union of objects that you have for AorB . And in fact it looks like this is a known issue , but currently TypeScript isn't able to make this inference.


To fix this, I see a few options. First, you can just use a single interface for AorB where the inner property is given the type A1 | B1 A1 | B1 :

interface AorB {
  foo: number;
  bar: string;
  inner: A1 | B1;
}

As another option, you can modify the way in which you construct the returned object in your test function to clearly indicate to typescript that the resulting object has a fixed inner property:

function test2(): AorB {
  let outer = {
    foo: 42,
    bar: 'plop',
  };

  if (Math.random()) {
    // Clearly has type A
    return {
      ...outer,
      inner: {plop: 4}
    }
  } else {
    // Clearly has type B
    return {
      ...outer,
      inner: {hop: 43}
    }
  }
}

This way, it is obvious to the TS compiler that one branch returns type A and the other type B , which matches cleanly to the return type of AorB .

And finally, since we humans can see that the two types are in fact equivalent, you can always just ignore the error:

function test(): AorB {
  let inner: A1 | B1;

  if (Math.random()) {
    inner = {plop: 4};
  } else {
    inner = {hop: 43};
  }

  // @ts-ignore: This is equivalent to the type AorB after distributing the inner union...
  return {
    foo: 42,
    bar: 'plop',
    inner
  };
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM