Home > database >  TypeScript alias does not "remember" unused generic type params or aliased type
TypeScript alias does not "remember" unused generic type params or aliased type

Time:04-06

Please forgive my verbosity in asking this question:

A library I'm using (which is an abstraction of redux), called easy-peasy, exposes the following types to be used in conjunction with one another. The Thunk type has several unused generic params, which seem to exist only to provide inference for the thunk function's genreic params (which match the Thunk type's params exactly):

export type Thunk<
  Model extends object, // not used
  Payload = undefined,
  Injections = any, // not used
  StoreModel extends object = {}, // not used
  Result = any
> = {
  type: 'thunk';
  payload: Payload;
  result: Result;
};

export function thunk<
  Model extends object = {},
  Payload = undefined,
  Injections = any,
  StoreModel extends object = {},
  Result = any
>(
  thunk: (
    actions: Actions<Model>,
    payload: Payload,
    helpers: Helpers<Model, StoreModel, Injections>,
  ) => Result,
): Thunk<Model, Payload, Injections, StoreModel, Result>;

The recommended usage would be something like:

type Injections = {
  someFunc: () => "foo";
}

const someThunk: Thunk<LocalModel, string, Injections, StoreModel, Promise<void>> = thunk(
  (actions, payload, { injections, getState, getStoreState }) => {
     // do something
     const state = getState(); // returns local state
     const storeState = getStoreState(); // returns global / store state
     const foo = injections.someFunc(); // foo
  }
);

However, if you try to create an alias of the Thunk type to have a less verbose definition, all of the generic params that don't get "used" by the Thunk type itself (Model, StoreModel, and Injections), seem to get lost, and actions, injections, getState (depends on Model type), and getStoreState (depends on StoreModel type) are no longer typed (they become any).

type LocalThunk<TPayload, TResult> = Thunk<
  LocalModel,
  TPayload,
  Injections,
  StoreModel,
  TResult
>;

const someThunk: LocalThunk<string, Promise<void>> = thunk(
  (actions, payload, { injections, getState, getStoreState }) => {
     // do something
     const state = getState(); // any
     const storeState = getStoreState(); // any
     const foo = injections.someFunc(); // any
  }
);

The best I can figure is that this is because the alias doesn't "remember" the types that don't actually get used in the Thunk type.

I have a few workarounds, so I'm not really looking for that here. What I'm interested in is if anyone can provide a more well substantiated reason as to why this is, and if this should be considered a bug, and if perhaps TypeScript github is a better place to raise this issue.

Here's a small repro so you can see this in action: https://codesandbox.io/s/shy-worker-qpi5lr?file=/src/store.tsx

Any info or documentation to support why this doesn't work would be helpful!

CodePudding user response:

This is because TS can infer the generics from the return value like so,

export function thunk<
  Model extends object = {},
  Payload = undefined,
  Injections = any,
  StoreModel extends object = {},
  Result = any
>(
  thunk: (
    actions: Actions<Model>, //Applies generic to here
    payload: Payload,
    helpers: Helpers<Model, StoreModel, Injections>, //and here
  ) => Result,
): Thunk<Model, Payload, Injections, StoreModel, Result>; //Infers the generic from the return value

But cannot do so when you encapsulate it, (because as jcalz pointed out, TS is not nominally based).

export function thunk<...>(
  thunk: (
    actions: Actions<Model>, //How do I infer this?
    payload: Payload,
    helpers: Helpers<Model, StoreModel, Injections>, //Kaboom!
  ) => Result,
): ThunkAlias<...>; //Uh oh, there is no Model value!

You'll have to create a wrapper function as a workaround. Unfortunately, since easy-peasy does not export all the utility types to create thunk this is not possible

Specifically, this line: helpers: Helpers<Model, StoreModel, Injections> is what will cause most of the headache in trying to do this. Because they don't export the Helpers type.

There is a feature released for TS 4.7 which allows you to derive type parameters from typed functions. https://github.com/microsoft/TypeScript/pull/47607. We can use this pattern to get the parameters of thunk. So we can do this once it is released, or if you are OK with running a developmental version of TS as a dependency. Install using npm/yarn install/add typescript@next

type ThunkAlias<TPayload = never, TReturn = any> = Thunk<
  StoreModel,
  TPayload,
  Deps,
  never,
  TReturn
>

export function thunkAlias<
  Model extends object = StoreModel,
  Payload = undefined,
  Injections = Deps,
  SModel extends object = {},
  Result = any
>(
  args: Parameters<typeof thunk<Model, Payload, Injections, SModel, Result>>[0],
): ThunkAlias<Payload, Result> {
  return thunk<Model, Payload, Injections, SModel, Result>(args)
}

You can view a demo version which emulates this here: TS Playground

The workaround is to simply copy the Helpers type, and redefine your wrapper function instead based on that. (and import any other types it needs)

// Yoinked from easy-peasy 
type Helpers<Model extends object, StoreModel extends object, Injections> = {
  dispatch: Dispatch<StoreModel>;
  fail: AnyFunction;
  getState: () => State<Model>;
  getStoreActions: () => Actions<StoreModel>;
  getStoreState: () => State<StoreModel>;
  injections: Injections;
  meta: Meta;
};

export function thunkAlias<
  Model extends object = StoreModel,
  Payload = undefined,
  Injections = Deps,
  SModel extends object = {},
  Result = any
>(
  _thunk: (
    actions: Actions<Model>,
    payload: Payload,
    helpers: Helpers<Model, SModel, Injections>,
  ) => Result,
): ThunkAlias<Payload, Result> {
  return thunk<Model, Payload, Injections, SModel, Result>(args)
}

P.S. but at some point, you have to ask yourself whether all this work^, is really worth it, for some less verbosity elsewhere? Depends on who you ask I suppose...

  • Related