In the code snippet provided below, there is an attempt to strictly enforce return types based on the returnType
property of the action
argument. The goal is to ensure that the return type matches the specific returnType
for each individual action
, rather than just any generic returnType
.
Please refer to the Typescript Playground link for a better understanding.
// Sample code from a library
export declare type ActionCreator<T extends string = string> = (
...args: any[]
) => {
type: T;
};
export declare type ActionCreatorMap<T> = { [K in keyof T]: ActionType<T[K]> };
export declare type ActionType<
ActionCreatorOrMap
> = ActionCreatorOrMap extends ActionCreator
? ReturnType<ActionCreatorOrMap>
: ActionCreatorOrMap extends object
? ActionCreatorMap<ActionCreatorOrMap>[keyof ActionCreatorOrMap]
: never;
// Custom implementation starts here:
type GameActionTypes = "type1" | "type2";
type GameplayAction<T extends string, P, R> = P extends void
? { type: T; returnType: R }
: { type: T; payload: P; returnType: R };
function action<R = void>() {
return function<T extends GameActionTypes, P = undefined>(
type: T,
payload?: P
): GameplayAction<T, P, R> {
return { type, payload } as any;
};
}
const action1 = () => action()("type1", { a: 1, b: 2 });
const action2 = () => action<{ foo: "bar" }>()("type2", { c: 3, e: 4 });
type gameActions = typeof action1 | typeof action2;
// Narrow down a union by a specified tag
export type FindByTag<Union, Tag> = Union extends Tag ? Union : never;
// These cases work properly
type TEST1 = FindByTag<ActionType<gameActions>, { type: "type1" }>;
type TEST2 = FindByTag<ActionType<gameActions>, { type: "type2" }>;
export function executeAction<T extends GameActionTypes>(
action: ActionType<gameActions>
): FindByTag<ActionType<gameActions>, { type: T }>["returnType"] {
if (action.type === "type1") {
// The enforced return type is `void`
return;
} else if (action.type === "type2") {
//////////////// This should result in failure!!!
// Expected return type: {foo: "bar"}
return;
}
}