Enforce that an array is exhaustive over a union type

TypeScript doesn’t really have direct support for an “exhaustive array”. You can guide the compiler into checking this, but it might be a bit messy for you. A stumbling block is the absence of partial type parameter inference (as requested in microsoft/TypeScript#26242). Here is my solution:

type Furniture="chair" | 'table' | 'lamp' | 'ottoman';

type AtLeastOne<T> = [T, ...T[]];

const exhaustiveStringTuple = <T extends string>() =>
  <L extends AtLeastOne<T>>(
    ...x: L extends any ? (
      Exclude<T, L[number]> extends never ? 
      L : 
      Exclude<T, L[number]>[]
    ) : never
  ) => x;


const missingFurniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp');
// error, Argument of type '"chair"' is not assignable to parameter of type '"ottoman"'

const extraFurniture = exhaustiveStringTuple<Furniture>()(
  'chair', 'table', 'lamp', 'ottoman', 'bidet');
// error, "bidet" is not assignable to a parameter of type 'Furniture'

const furniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp', 'ottoman');
// okay

As you can see, exhaustiveStringTuple is a curried function, whose sole purpose is to take a manually specified type parameter T and then return a new function which takes arguments whose types are constrained by T but inferred by the call. (The currying could be eliminated if we had proper partial type parameter inference.) In your case, T will be specified as Furniture. If all you care about is exhaustiveStringTuple<Furniture>(), then you can use that instead:

const furnitureTuple =
  <L extends AtLeastOne<Furniture>>(
    ...x: L extends any ? (
      Exclude<Furniture, L[number]> extends never ? L : Exclude<Furniture, L[number]>[]
    ) : never
  ) => x;

Playground link to code

Leave a Comment