0

I have an enum of operations (that I can't change, unfortunately):

enum OpType {
  OpA = 0,
  OpB = 1,
}

…and a set of types for objects that carry the data needed for each operation:

type A = {
  readonly opType: OpType.OpA;
  readonly foo: number;
};

type B = {
  readonly opType: OpType.OpB;
  readonly bar: string;
};

…and finally a handler function that ensure that each operation is handled:

type Ops = A | B;

export const ensureExhaustive = (_param: never) => {};

export const handleOp = (op: Ops) => {
  switch (op.opType) {
    case OpType.OpA:
      if (op.foo < 80) { /* … */ }
      break;
    case OpType.OpB:
      if (op.bar === 'foo') { /* … */ }
      break;
    default:
      ensureExhaustive(op);
  }
}

However, this handleOp function only really assures that we handle what was explicitly added to the Ops union – the connection to the OpType enum has been lost, so if a OpC = 2 is added to the enum, it won't be detected that this isn't handled.

How can I “connect” the enum values (probably through an Ops type) to the switch statement in handleOp to ensure that each value is handled?

beta
  • 2,380
  • 21
  • 38
  • Sure, just use the same idea as your ensureExhaustive function: https://tsplay.dev/mbKOdW – kelsny Jan 17 '23 at 20:51
  • Oh, and you can be even fancier and *reuse* the ensureExhaustive function: https://tsplay.dev/mAdAvN – kelsny Jan 17 '23 at 20:54
  • @vera. This is fantastic! Thanks! It even makes sense (after staring at it for a while). If you make it an answer, I'll click the checkmark! :) – beta Jan 19 '23 at 11:36

1 Answers1

1

You could just recreate the ensureExhaustive function as a type:

type EnsureExhaustive<T extends never> = T;

Then you could define a dummy type that just makes sure something is never:

type OpsMatchesEnum = EnsureExhaustive<Exclude<OpType, Ops["opType"]>>;

Conveniently, there exists a utility Exclude that already has the behavior we want:

Exclude<1 | 2 | 3, 1 | 2>;     // 3
Exclude<1 | 2 | 3, 1 | 2 | 3>; // never

In other words, the result of this type is never if the second argument encompasses the first. Translating to our use case, we want this to be never if Ops["opType"] uses all members of OpType.

Another trick we can do is to make ensureExhaustive generic, so that it would have the signature

export const ensureExhaustive = <T extends never>(_param: T) => {};

so we can then utilize instantiation expressions that were introduced in 4.7, removing the need for an EnsureExhaustive type:

type OpsMatchesEnum = typeof ensureExhaustive<Exclude<OpType, Ops["opType"]>>;

Of course, if you have noUnusedLocals enabled or a linter, this dummy type might cause an error or warning.

kelsny
  • 23,009
  • 3
  • 19
  • 48