I have two string enums that have the same variants, but have different string values.
They get passed to the same initial function, which accepts the entire enum (which I think I can do using typeof
, but each enum should have a different callback. What seems to be the best way to type the initial function?
This code that I have doesn't work, as it complains that I can't use typeof
with my T
generic parameter.
enum OpsA {
Start = 'a_start',
Stop = 'a_stop'
}
enum OpsB {
Start = 'b_start',
Stop = 'b_stop'
}
type EitherOps = OpsA | OpsB
function doSomething <T extends EitherOps, U extends typeof T>(action: U, callback: (a: T) => void) {
console.log(action.Start)
callback(action.Start)
console.log(action.Stop)
callback(action.Stop)
}
// does something special for OpsA
const callbackForA = (a: OpsA) => console.log(a)
// does something else special for OpsB
const callbackForB = (b: OpsB) => console.log(b)
doSomething(OpsA, callbackForA)
doSomething(OpsB, callbackForB)
CodePudding user response:
Your problem is that T
is a type and not a value, so typeof T
is a category error. Your confusion probably stems from the fact that enum
declarations bring into scope both a named value and a named type, and the names happen to be the same. But there's no general rule that a value and a type with the same name have any relationship to each other.
For example, enum OpsA { /* ... */ }
declares both the value OpsA
(an object with property keys Start
and Stop
) and the type OpsA
(a union of the types of the property values of the enum object). When you write typeof OpsA
as a type you are talking about the type of the enum object, while when you write OpsA
as a type you are talking about the type of the enum object's property values.
But the fact that the type OpsA
is the property type of the object type typeof OpsA
cannot be generalized the way you are presumably trying to do. There is no rule that says "given the type T
there is a type typeof T
whose properties are of type T
." See an answer to a related question for more information.
Okay, if you can't write U extends typeof T
, what can you do? You want U
to be an object type whose property keys are "Start"
or "Stop"
. Let's get a type for those keys first. Since both ObjA
and ObjB
value have these keys, we can write that as either keyof typeof ObjA
or keyof typeof ObjB
:
type OpsKeys = keyof typeof OpsA;
// type OpsKeys = "Start" | "Stop"
If we want to say that U
should be an object type with keys of OpsKeys
and values of type T
, then we can use the Record<K, V>
utility type: U extends Record<OpsKeys, T>
. Like this:
function doSomething<
T extends EitherOps,
U extends Record<OpsKeys, T>
>(action: U, callback: (a: T) => void) {
console.log(action.Start)
callback(action.Start)
console.log(action.Stop)
callback(action.Stop)
}
Now everything works as desired inside doSomething()
. Let's make sure that it also works for callers:
doSomething(OpsA, callbackForA) // okay
doSomething(OpsB, callbackForB) // okay
doSomething(OpsA, callbackForB) // error
Looks good. The compiler warns if you pass in a callback
argument that's inappropriate for the action
argument.