typescriptgenerics

Is is possible to create a modified version of a generic function in TypeScript


Let's say I have a generic function with the following definition:

function createSlice<
  State,
  CaseReducers extends SliceCaseReducers<State>,
  Name extends string,
  Selectors extends SliceSelectors<State>,
  ReducerPath extends string = Name,
>(
  options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>
): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
  return options as Slice<State, CaseReducers, Name, ReducerPath, Selectors>;
}

// Let's define some types for simplicity:

export type SliceCaseReducers<State> = State & { someField: State };
export type SliceSelectors<State> = State & { someOtherField: State };
export type CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors> = {
  a: State;
  b: CaseReducers;
  c: Name;
  d: ReducerPath;
  e: Selectors;
};
export type Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
  a: State;
  b: CaseReducers;
  c: Name;
  d: ReducerPath;
  e: Selectors;
};

And I want to create a createSliceWithSideEffects function which would look like this:

const doSomeSideEffects = (slice: unknown) => {
  console.log(slice);
};

const createSliceWithSideEffects = (options: any) => {
  const slice = createSlice(options);

  doSomeSideEffects(slice);

  return slice;
};

But the problem is typing that function. The only solution I see is to copy-paste all of the createSlice generic parameters and define the createSliceWithSideEffects function like this:

const doSomeSideEffects = (slice: unknown) => {
  console.log(slice);
};

const createSliceWithSideEffects = <
  State,
  CaseReducers extends SliceCaseReducers<State>,
  Name extends string,
  Selectors extends SliceSelectors<State>,
  ReducerPath extends string = Name,
>(
  options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>
): Slice<State, CaseReducers, Name, ReducerPath, Selectors> => {
  const slice = createSlice(options);

  doSomeSideEffects(slice);

  return slice;
};

Which is quite cumbersome and requires updating the definition each time the definition of createSlice changes. Ahother problem might be that SliceCaseReducers and SliceSelectors are not exported by a library and you would have to copy these types' definitions as well.

Is there a way to do it without copying all of the parameters? So it could look something like this while also preserving the generic types:

const createSliceWithSideEffects = modifiedGenericFunction(createSlice, (...params) => {
  const slice = createSlice(...params);

  doSomeSideEffects(slice);

  return slice;
});

You can play with it in a typescript playground


Solution

  • You can use TypeScript's support for higher order type inference from generic functions to transform generic functions in a way that preserves their type parameters. It looks like you want modifiedGenericFunction(originalFunc, modifiedFunc) to return modifiedFunc at runtime, but to use originalFunc's call signature including all the generics. That can be done this way:

    function modifiedGenericFunction<A extends any[], R>(
      originalFunc: (...a: A) => R,
      modifiedFunc: (...a: A) => R
    ): (...a: A) => R {
      return modifiedFunc
    }
    

    By making modifiedGenericFunction generic in the parameters type A and the return type R of the input functions, TypeScript will automatically "lift" the generic call signature of originalFunc into A and R in the way you want:

    const createSliceWithSideEffects = modifiedGenericFunction(createSlice, (...params) => {
      const slice = createSlice(...params);
    
      doSomeSideEffects(slice);
    
      return slice;
    });
    
    /* const createSliceWithSideEffects: <
      State, CaseReducers extends SliceCaseReducers<State>, 
      Name extends string, Selectors extends SliceSelectors<State>, 
      ReducerPath extends string = Name
      >(options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>) => 
      Slice<State, CaseReducers, Name, ReducerPath, Selectors> */
    

    Looks good. The type of createSliceWithSideEffects is exactly the same as createSlice, including the generic constraints and default type arguments.

    Playground link to code