How to set types based on a value of a property in another parameter?
How should I set a type based on a value of a property of another parameter in a method? I'm relatively new to TypeScript.
This is what I tried:
class SomeClass {
// constructor, other properties, etc
listen<K extends keyof ClientEvents>({ event: K }: ClientEvent, listener: (...args: ClientEvents[K]) => any)
}
Here, ClientEvents
looks like this:
interface ClientEvents {
"event-1": [data: number]
"event-2": [data: number]
"event-3": [data: any]
}
And ClientEvent
(no s) looks like this:
interface ClientEvent {
event: keyof ClientEvents
anotherProperty: any // I don't want to share all the properties
}
Currently, the event
field does autofill to event-1
, but the first parameter to the function shows up as type any
, but it should be type number
. How can I fix this without needing more parameters?
When changing one of the data
s from inside of ClientEvents
to type string
, it shows up as type string | number
.
class SomeClass {
listen<K extends keyof ClientEvents>({ event: K }: ClientEvent, listener: (...args: ClientEvents[K]) => any): void;
}
interface ClientEvents {
"event-1": [data: number]
"event-2": [data: string]
}
interface ClientEvent {
event: keyof ClientEvents
anotherProperty: any
}
new SomeClass().listen({
event: "event-1",
anotherProperty: 1
}, (arg) => {
arg // type is string | number
})
Here is a Playground Link
See the FAQ entry on destructuring assignment and type annotations for a possibly-authoritative answer.
Your problem is that the K
in { event: K }: ClientEvent
isn't what you think it is. In JavaScript, if you have a function parameter like { event: K }
, you are using destructuring assignment with variable renaming, so you will copy the event
property of that function argument into a variable named K
:
function foo({event: K}: ClientEvent) {
// variable -----> ^
console.log(K.toUpperCase());
// ^ <---- see?
}
foo({event: "event-1", anotherProperty: 123}) // EVENT-1
Because JavaScript supports this, so does TypeScript (as you can see by the ClientEvent
type annotation), and therefore the call signature itself just uses K
as a dummy parameter name that it ignores.
And of course, that K
cannot be used as an inference site for the type parameter K
of the same name, since they are unrelated.
You were attempting to use K
as a type annotation for the event
property. But that's not supported. In fact there is currently no way to put type annotations inside the destructured object itself. There is an open feature request at microsoft/TypeScript#29526 asking for some way to do this (maybe... double colon? like {event::K}
) but for now it's impossible.
If you want to give a type to the event
property, you will have to do it in the actual type annotation, after the destructured object:
{ event }: { event: K }
And if you want to preserve the fact that the whole thing needs to be a ClientEvent
, you will need to express that with something like an intersection type
{ event }: { event: K } & ClientEvent
Or possibly by extending your ClientEvent
interface if that looks nicer:
interface ClientEventFor<K extends keyof ClientEvents> extends ClientEvent {
event: K
}
declare class SomeClass {
listen<K extends keyof ClientEvents>(
{ event }: ClientEventFor<K>,
listener: (...args: ClientEvents[K]) => any): void;
}
And then things will start working for you:
new SomeClass().listen(
{ event: "event-1", anotherProperty: 1 },
(arg) => { arg.toFixed() } // okay, arg is number here
)
Playground link to code