Reputation: 428
Currently creating events in my TypeScript app looks like this:
// This function creates event creators
function defineEvent<E extends {type:string, payload:any}>(type:E["type"]) {
return (payload:E["payload"]) => ({
type,
payload
})
}
// This function creates foo events
const fooCreator = defineEvent<{
type:"foo",
payload: {
foo:string
}
}>("foo");
// This function creates bar events
const barCreator = defineEvent<{
type:"bar",
payload:{
bar:string
}
}>("bar");
// Example events
const fooEvent = fooCreator({foo:'foo'});
const barEvent = barCreator({bar:'bar'});
// Create a union type to switch over
type AppEvent = ReturnType<typeof fooCreator> | ReturnType<typeof barCreator>;
// Example of switching with a discriminated union
function switchOnEvents(event: AppEvent) {
switch(event.type){
case "foo":
// compiler is happy about payload having a foo property
return event.payload.foo.toLowerCase();
case "bar":
// compiler is happy about payload having a bar property
return event.payload.bar.toLowerCase();
}
}
This works and is okish however this means I need to specify the event type twice when defining event creators which in some ways could be considered redundant.
const fooCreator = defineEvent<{
type:"foo", // defining type here
payload: {
foo:string
}
}>("foo"); // also here to create the actual string value
Is it possible to create a return type for the creator function that extracts the string literal from the input type
argument to the factory function?
Which would mean the example above would still work if the event creators were provided like so:
const barCreator = defineEvent<{
payload:{
bar:string
}
}>("bar");
I am guessing I need to merge two types but somehow infer the string literal.
However this doesn't work:
function defineEvent<E extends { payload:any}, ET extends string>(type:ET) {
type RE = E & {type: ET};
return (payload:E["payload"]) => {
return ({
type,
payload
} as any ) as RE
}
}
The compiler wants me to pass a second type argument.
const fooCreator = defineEvent<{
payload: {
foo:string
} //Expected 2 type arguments, but got 1.
}>("foo");
Anyone know how to do this or if it is impossible?
Upvotes: 0
Views: 518
Reputation: 329453
The problem here is that TypeScript does not yet support partial type parameter inference; your defineEvent()
function fundamentally depends on two types: the payload type (call it P
), and the, uh, "type" string literal type (call it T
). You want to specify P
but have the compiler infer T
, and that's not supported. You can either specify both of them, or the compiler can try to infer both of them.
So there are two possible workarounds I know of. One is to use currying, where a function returns another function. The first function is generic in P
, which you will specify, and the returned function is generic in T
, which will be inferred from the function's argument. Like this:
const defEvent = <P>() => <K extends string>(type: K) => (payload: P) => ({
type,
payload
});
const fooCreator = defEvent<{ foo: string }>()("foo");
const barCreator = defEvent<{ bar: string }>()("bar");
This gives you the same fooCreator
and barCreator
objects you had before, and you specify exactly the payload type and the type string. The awkwardness here is in the extra chained function call.
The other workaround is to use a dummy payload parameter which allows the compiler to infer both T
and P
. The dummy parameter is not used by the body of the defineEvent()
function; it's only used by the type system. That also means you don't really need to pass an actual value of type P
in; you can use a type assertion to pass something like null
or undefined
:
const defEvent = <P, T extends string>(payloadDummy: P, type: T) => (
payload: P
) => ({
type,
payload
});
const fooCreator = defEvent(null! as { foo: string }, "foo");
const barCreator = defEvent(null! as { bar: string }, "bar");
This is again, the same fooCreator
and barCreator
you had. The awkwardness here, of course, is using the dummy parameters. I tend to prefer currying over dummying, but that's up to you.
Okay, hope that helps; good luck!
Upvotes: 1