Suppose the following sample code:
type Action = "GET" | "POST" | "PUT";
type Handler<A extends Action> = (action: A) => void;
const handlers: Partial<Record<Action, Handler<Action>>> = { };
function register<A extends Action>(action: A, handler: Handler<A>) {
/*
Error:
Type 'Handler<A>' is not assignable to type 'Partial<Record<Action, Handler<Action>>>[A]'.
Type 'Handler<A>' is not assignable to type 'Handler<Action>'.
Type 'Action' is not assignable to type 'A'.
'Action' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'Action'.
Type '"GET"' is not assignable to type 'A'.
'"GET"' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'Action'.
*/
handlers[action] = handler;
}
To my understanding, this error above occurs because A
allows for types larger than Action
(e.g. it could be Action | "DELETE"
), but my handlers
record only allows for exactly
the Action
union type. There are ways I can sort of work around this:
- Cast down
handler
internally. This quiets down the compiler without actually solving the problem, as the user can still pass a larger type intoregister
. Plus, casts are never ideal :) - Make the function concrete,
register(action: Action, handler: Handler<Action>)
. This now means thataction
andhandler
don't have to agree in their types, which can lead to runtime errors.
As neither of these workarounds fully solve the issue, is there a way for me to enforce that action
and handler
both use the same A
, while also not allowing for A
to be any larger than Action
?
Edit:
I've actually found an even smaller minimum repro that gives the same error:
function foobar<T extends "foo" | "bar">(func: (arg: T) => void): (arg: "foo" | "bar") => void {
return func;
}
where the return statement gives the same error as above. This reveals the actual issue: T
can actually be smaller than the union, so you might end up passing in a function that can handle less cases than what's expected.