runtypes icon indicating copy to clipboard operation
runtypes copied to clipboard

`Union` type checks successfully when all members individually do not when using `Intersect`

Open adriancooney opened this issue 3 years ago • 0 comments

Hi folks. So I've noticed an issue where, given some input, a Union(A, B, C) is type checking when each of its members A, B and C all fail type checking individually.

Minimal reproduction:

import { Record, Literal, Intersect, Union, Static } from "runtypes";

const A = Record({
  foo: Literal("bar"),
});

const B = Intersect(
  A,
  Record({
    bar: Literal("foo"),
  })
);

const C = Intersect(
  A,
  Record({
    foobar: Literal("foobar"),
  })
);

const ABC = Union(A, B, C);

// type ABCType =
// | {
//     foo: "bar";
//   }
// | ({
//     foo: "bar";
//   } & {
//     bar: "foo";
//   })
// | ({
//     foo: "bar";
//   } & {
//     foobar: "foobar";
//   });
type ABCType = Static<typeof ABC>;

// Typescript Error:
//   Type '{ bar: "foo"; }' is not assignable to type '{ foo: "bar"; } | ({ foo: "bar"; } & { bar: "foo"; }) | ({ foo: "bar"; } & { foobar: "foobar"; })'.
//     Type '{ bar: "foo"; }' is not assignable to type '{ foo: "bar"; } & { bar: "foo"; }'.
//       Property 'foo' is missing in type '{ bar: "foo"; }' but required in type '{ foo: "bar"; }'.ts(2322)
const input: ABCType = {
  bar: "foo",
};

console.log(ABC.guard(input)); // true
console.log(A.guard(input)); // false
console.log(B.guard(input)); // false
console.log(C.guard(input)); // false

Typescript correctly spots the error when assigning input to ABCType however Runtypes ABC.guard(input) does not.

If we remove the Intersect, it works as expected.

import { Record, Literal, Union, Static } from "runtypes";

const A = Record({
  foo: Literal("bar"),
});

const B = Record({
  foo: Literal("bar"),
  bar: Literal("foo"),
});

const C = Record({
  foo: Literal("bar"),
  foobar: Literal("foobar"),
});

const ABC = Union(A, B, C);

// type ABCType =
//   | {
//       foo: "bar",
//     }
//   | {
//       foo: "bar",
//       bar: "foo",
//     }
//   | {
//       foo: "bar",
//       foobar: "foobar",
//     };
type ABCType = Static<typeof ABC>;

const input = {
  bar: "foo",
};

console.log(ABC.guard(input)); // false
console.log(A.guard(input)); // false
console.log(B.guard(input)); // false
console.log(C.guard(input)); // false

Sounds like there's a bug when Intersect'ing the records. Maybe I'm misusing the feature?

adriancooney avatar Mar 10 '22 11:03 adriancooney