😊 Hi everyone!
interface Data {
date: Date;
message?: string;
}
interface DataB {
dateB: Date;
messageB?: string;
}
class DataResolver {
public submit(): Data {
return { date: new Date() };
}
public submitB(): DataB {
return { dateB: new Date() };
}
}
interface StringKeyedObject {
[key: string]: any;
}
class Handler<C extends StringKeyedObject> {
send<
K extends Extract<keyof C, string>,
L extends ReturnType<Extract<keyof K, object>>,
>(eventName: K, arg: L) {
//
}
}
const handler = new Handler<DataResolver>();
handler.send('submit', null);
I really would like to set the second arg
parameter depending on the first value
paramter. In VS Code autocomplete suggests submit
and submitB
as intended. 😊 Now if I select submit
I would like to be arg of type Data
. If I select submitB
the type should be DataB
.
My attempt with generic type L extends ReturnType<Extract<keyof K, object>>
does not work at all. Obviously.
I hope you'll find a solution! Thanks an all the best! 😊
I would write it like this:
class Handler<C extends StringKeyedObject> {
send<K extends Extract<keyof C, string>>(
value: K, arg: C[K] extends (...args: any) => infer R ? R : never) {
// do stuff
}
}
Here the arg
parameter type depends on K
. It is essentially the same as ReturnType<C[K]>
, so let's first look at that version first:
class Handler<C extends StringKeyedObject> {
send<K extends Extract<keyof C, string>>(
value: K, arg: ReturnType<C[K]> ) {
// do stuff
}
}
The type C[K]
is an indexed access type meaning "the property type you'd read when indexing into an object of type C
with a key of type K
". It is presumably what you were trying to do with keyof K
, except that the keyof
operator on K
would give you "the keys of the keys of C", which is like, whatever keys a string has... toUppercase
and such. Not what you wanted.
And note that, unless you have a special reason, you don't need to have a separate type parameter for arg
. You could have written L extends ReturnType<C[K]>
and used L
, but we don't need some arbitrary subtype of ReturnType<C[K]>
for this to work, so there's no reason to add another type parameter.
So, given this definition, let's make sure it works:
class DataResolver {
public submit(): Data { return { date: new Date() }; }
public submitB(): DataB { return { dateB: new Date() }; }
a = 2 // I added this
}
const handler = new Handler<DataResolver>();
handler.send("submit", { date: new Date() }); // okay
handler.send("submitB", { dateB: new Date() }); // okay
handler.send("a", "wha") // <-- shouldn't compile but it does
Everything works the way you want, except... if your DataResolver
happens to have a non-function property, we don't want the compiler to accept any call to handler.send()
for that property. One way to do that is to check whether the property value is a function type, and if so, use its return type; and if not, use the impossible never
type. That brings us back to:
class Handler<C extends StringKeyedObject> {
send<K extends Extract<keyof C, string>>(
value: K, arg: C[K] extends (...args: any) => infer R ? R : never) {
// do stuff
}
}
where arg
is a conditional type that depends on C[K]
. The use of infer
lets us extract out the return type as its own type parameter R
and use it.
By the way, this is very similar to how ReturnType<T>
is defined:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
except that this version gives the any
type when T
isn't a function, and since any
accepts anything, the call to handler.send("a", "wha")
succeeds when it shouldn't.
Okay, let's test it one more time:
const handler = new Handler<DataResolver>();
handler.send("submit", { date: new Date() }); // okay
handler.send("submitB", { dateB: new Date() }); // okay
handler.send("a", "wha") // error!
// -------------> ~~~~~
// Argument of type 'string' is not assignable to parameter of type 'never'.
Looks good. There are other ways to harden send
against bad inputs (we could make it so that you're not even allowed to give it keys corresponding to non-functions) but I don't want to digress even further from the question as asked.