Like many good solutions in programming, you achieve this by adding a layer of indirection.
Specifically, what we can do here is add a table between action tags (i.e. "Example"
and "Another"
) and their respective payloads.
type ActionPayloadTable = {
"Example": { example: true },
"Another": { another: true },
}
then what we can do is create a helper type that tags each payload with a specific property that maps to each action tag:
type TagWithKey<TagName extends string, T> = {
[K in keyof T]: { [_ in TagName]: K } & T[K]
};
Which we’ll use to create a table between the action types and the full action objects themselves:
type ActionTable = TagWithKey<"type", ActionPayloadTable>;
This was an easier (albeit way less clear) way of writing:
type ActionTable = {
"Example": { type: "Example" } & { example: true },
"Another": { type: "Another" } & { another: true },
}
Now we can create convenient names for each of out actions:
type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];
And we can either create a union by writing
type MyActions = ExampleAction | AnotherAction;
or we can spare ourselves from updating the union each time we add a new action by writing
type Unionize<T> = T[keyof T];
type MyActions = Unionize<ActionTable>;
Finally we can move on to the class you had. Instead of parameterizing on the actions, we’ll parameterize on an action table instead.
declare class Example<Table> {
doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}
That’s probably the part that will make the most sense – Example
basically just maps the inputs of your table to its outputs.
In all, here’s the code.
/**
* Adds a property of a certain name and maps it to each property's key.
* For example,
*
* ```
* type ActionPayloadTable = {
* "Hello": { foo: true },
* "World": { bar: true },
* }
*
* type Foo = TagWithKey<"greeting", ActionPayloadTable>;
* ```
*
* is more or less equivalent to
*
* ```
* type Foo = {
* "Hello": { greeting: "Hello", foo: true },
* "World": { greeting: "World", bar: true },
* }
* ```
*/
type TagWithKey<TagName extends string, T> = {
[K in keyof T]: { [_ in TagName]: K } & T[K]
};
type Unionize<T> = T[keyof T];
type ActionPayloadTable = {
"Example": { example: true },
"Another": { another: true },
}
type ActionTable = TagWithKey<"type", ActionPayloadTable>;
type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];
type MyActions = Unionize<ActionTable>
declare class Example<Table> {
doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}
const items = new Example<ActionTable>();
const result1 = items.doSomething("Example");
console.log(result1.example);