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?
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.