I have a custom type like this (this code doesn't work):

type MyType =
  | {
      foo: string;
    }
  | {
      foo: string;
      barPre: string;
      barPost: string;
    }
  | {
      foo: string;
      quxPre: string;
      quxPost: string;
    }
  | {
      foo: string;
      barPre: string;
      barPost: string;
      quxPre: string;
      quxPost: string;
    };

I basically want the following objects to be valid MyTypes:

const myThing1: MyType = { foo: 'foo' };
const myThing2: MyType = { foo: 'foo', barPre: 'barPre', barPost: 'barPost' };
const myThing3: MyType = { foo: 'foo', quxPre: 'quxPre', quxPost: 'quxPost' };
const myThing4: MyType = {
  foo: 'foo',
  barPre: 'barPre',
  barPost: 'barPost',
  quxPre: 'quxPre',
  quxPost: 'quxPost',
};

And the following objects should be invalid MyTypes:

const myThing5: MyType = {}; // missing 'foo'
const myThing6: MyType = { barPre: 'barPre', barPost: 'barPost' }; // missing 'foo'
const myThing7: MyType = { foo: 'foo', barPre: 'barPre' }; // if `barPre` exists, `barPost` must, too.
const myThing8: MyType = { barPre: 'barPre', barPost: 'barPost', quxPre: 'quxPre', quxPost: 'quxPost' }

I got the correct type hints, by typing it like this:

type MyType = {
  foo: string;
} & ({
  barPre?: undefined;
  barPost?: undefined;
} | {
  barPre: string;
  barPost: string;
}) & ({
  quxPre?: undefined;
  quxPost?: undefined;
} | {
  quxPre: string;
  quxPost: string;
});

But this seems very tedious. Is there an easier way to achieve this?

1

There are 1 best solutions below

0
On

This is due to the weird excess property checking Typescript does for unions. See https://github.com/microsoft/TypeScript/issues/36945

There is a strict union type you can define that works at least in your case. I've not tried this in earnest but you can read about it here.

Define a StrictUnion type as:

type UnionKeys<T> = T extends T ? keyof T : never
type StrictUnionHelper<T, TAll> = T extends T ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>> : never
type StrictUnion<T> = StrictUnionHelper<T, T>

and then use that in your union

type MyType = StrictUnion<A | B | C | D>

It's then reporting errors myThing5, 6, 7 and 8 as you'd expect.