Reputation: 1444
I am creating a function to create Redux actions (like createAction
from redux toolkit). I need a function that will return action generator and I would like this generator to be generic based on what types are provided to creator function.
const createGenericAction = <T extends string>(type: T) => <
A extends {},
B extends {}
>(
payloadGenerator: (a: A) => B
) => {
const factory = (payload: A) => ({
type,
payload: payloadGenerator(payload),
});
factory.toString = (() => type) as () => T;
return factory;
};
This is how the creator function looks like now (the toString
implementation is due to compatibility with redux-toolkit).
It is working okay when payloadGenerator
is not generic, so:
const someAction = createGenericAction('someAction')(
(payload: { a: number; b: string }) => payload
);
has correct type.
Although, when payloadGenerator
is generic, whole type inferring falls apart:
const someAction = createGenericAction('someAction')(
<T extends string>(payload: { value: T }) => payload
);
Argument of type '<T extends string>(payload: { value: T; }) => { value: T; }' is not assignable to parameter of type '(a: {}) => { value: string; }'.
Types of parameters 'payload' and 'a' are incompatible.
Property 'value' is missing in type '{}' but required in type '{ value: string; }'.ts(2345)
More complicated example
enum Element {
Elem1 = 'elem1',
Elem2 = 'elem2',
}
type ElementValueMapper = {
[Element.Elem1]: string;
[Element.Elem2]: number;
};
const someAction = createGenericAction('someAction')(
<T extends Element>(payload: { e: T; value: ElementValueMapper[T] }) =>
payload
);
Such action should allow calls:
someAction({ e: Element.Elem1, value: 'string' }); // okay
someAction({ e: Element.Elem2, value: 5 }); // okay
But disallow:
someAction({ e: Element.Elem1, value: 5 }); // error value should be type string
Upvotes: 1
Views: 276
Reputation: 1444
Ok, I've tracked that issue comes from reimplementing toString
of Function. The idea is that after assigning function to toString
, factory
is no longer treated as "TypeScript function", but as following interface:
{
(payload: A): {
payload: B;
};
toString(): A;
}
And part of this interface is correct, but whole type of factory
is not what I desire. Actually, every function in JS (so in TS also) is of function
type and comes from Function
prototype, which do contain toString
, but TypeScript does distinguish these types and Function
is not really often used.
Without too much off-topic, solution is to assert type to factory
:
const createGenericAction = <A extends string>(code: A) => <
A extends {},
B extends {}
>(
payloadGenerator: (a: A) => B
) => {
const factory: (payload: A) => { payload: B } = (payload: A) => ({
payload: payloadGenerator(payload),
});
factory.toString = () => code;
return factory;
};
Upvotes: 1