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...