Łukasz Szcześniak
Łukasz Szcześniak

Reputation: 1444

Inferring generic function type in TypeScript

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

Answers (1)

Łukasz Szcześniak
Łukasz Szcześniak

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

Related Questions