I'm trying to figure out how to eliminate some duplicate code when handling a discriminated union in a way that does not weaken the type checking:
const handlers = {
foo: (input: string): void => console.log(`foo ${input}`),
bar: (input: number): void => console.log(`bar ${input}`),
};
type Handlers = typeof handlers;
type Values<T> = T[keyof T];
type DiscriminatedInput = Values<{
[Id in keyof Handlers]: {type: Id; value: Parameters<Handlers[Id]>[0]};
}>;
const inputs: DiscriminatedInput[] = [
JSON.parse('{"type": "foo", "value": "hello world"}'),
JSON.parse('{"type": "bar", "value": 42}'),
];
for (const input of inputs) {
// This doesn't work:
//
// handlers[input.type](input.value);
// ^^^^^^^^^^^
// error: Argument of type 'string | number' is not assignable to
// parameter of type 'never'.
//
// So I do this instead, which has a lot of duplication and must be kept in
// sync with `handlers` above:
switch (input.type) {
case 'foo': handlers[input.type](input.value); break;
case 'bar': handlers[input.type](input.value); break;
default: ((exhaustiveCheck: never) => {})(input);
}
}
Inside the for
loop above, handlers[input.type]
is guaranteed to be a function whose first parameter always matches the type of input.value
, regardless of input.type
. It seems to me that TypeScript should be able to see that, but it doesn't.
Am I doing something wrong, or is this is a limitation of TypeScript?
If it's a limitation of TypeScript, is there an existing bug report? Is there something I can do to help TypeScript narrow input
to a foo
- or bar
-specific type so that I can eliminate that switch
statement? Or refactor DisciminatedInput
?
I could use a type assertion to weaken the type checking, but that adds complexity and reduces readability just to work around a language limitation. I'd rather work with the language instead of against it.