简体   繁体   中英

Mapped object types in Typescript

This is a generic typescript problem but I'll explain in terms of react-redux application that I am working on at the moment. If necessary I could come up with general example but hope this will suffice for now. I am trying to map actionCreators in redux-thunk react application into types that have dispatch and getState already injected to them. I have:

export interface AppThunkAction<TAction= {}, TDispatchResponse = void, TActionResponse = void> {
     (dispatch: (action: TAction) => TDispatchResponse, getState: () => ApplicationState): TActionResponse;
}

And my action creators look like this:

@typeName('BAR_ACTION')
export class BarAction extends Action {
    constructor() {
        super();
    }
}

@typeName('FOO_ACTION')
export class FooAction extends Action {
    constructor(public baz: string) {
        super();
    }
}

export const actionCreators = {
    foo: (baz: string): AppThunkAction<FooAction, void, Promise<void>> => (dispatch, getState): Promise<void> => {
        return new Promise((resolve, reject) =>
            resolve(dispatch(new FooAction(baz)))
        );
    },
    bar: (): AppThunkAction<BarAction, void, Promise<number>> => (dispatch, getState): Promise<number> => {
        return new Promise((resolve, reject) => {
            dispatch(new BarAction());
            resolve(3);
        });
    },
};

These objects are handled by redux-thunk middle-ware, so the actual types (that I need to specify for components) look like this

export type actionCreatorsTypes = {
    foo: (baz: string) => Promise<void>,
    bar: () => Promise<number>,
};

The problem is I have to manually write these type definitions and things get messy if I forget to change them, make a typo and so on.

I am therefore looking for automated way of handling this. A function or decorator that would iterate over this type and modify as if the dispatch and getState was injected. I've looked at mapped types in typescript but so far I struggle to create anything more complicated than simple pick or easy mapping.

Does anybody have a hint on how to approach this?

ORIGINAL ANSWER, OCTOBER 2017

TypeScript as of version 2.5 doesn't have great support for doing type operations with function types:

  • The lack of variadic kinds means you can't generalize over functions of different numbers of arguments. In your case, there's no easy way to write that the foo property will take a single string argument and the bar property takes no arguments, in a way that will map nicely with generics.

  • You also can't take an arbitrary function type and programmatically pull out its parameter or return types, the way you can with object properties using keyof and lookup types . There is a proposal which would give you this ability for function types, but it's not there yet.

  • Finally, you can't arbitrarily unwrap types in a way that would convert AppThunkAction<T, D, R> to R without something like mapped conditional types .

Workarounds exist for each of those but they are uglier/clunkier/less-maintainable than just manually specifying the function types the way you're doing now. I've gone down that route a few times and it's rarely worth it.

So that means that the conceptually simple operation of "take any function type (a:A, b:B, ...)=>AppThunkAction<T, D, R> and produce the related function type (a: A, b: B, ...)=>R " can't really be done. Sorry. 🙁


I do understand that manually specifying the types is redundant, but if you make some testing code you should at least catch errors if you change the types:

if (false as true) {
  // errors will appear in here if you leave out keys or change signatures
  const act: actionCreatorsTypes = {
    foo: (x) => actionCreators.foo(x)(null!, null!),
    bar: () => actionCreators.bar()(null!, null!)
  };
}

Just a thought. Good luck!


UPDATE, SEPTEMBER 2018

Well, since this question was asked, some key features have been introduced which change this from "you can't really do this" to "you can do this fairly easily":

  • TypeScript 2.8 gave us conditional types and type inference using the infer keyword , allowing us to pull parameter and return types out of function signatures, as well as converting AppThunkAction<T, D, R> to R .

  • TypeScript 3.0 gave us the ability to use tuples in rest/spread expressions , as a way of representing a type for "the list of function arguments" in a generic way. It's a good chunk of the way toward supporting variadic kinds, and enough for our purposes here.

Those cover the missing features from the original answer. Let's use them! Here's how we do it:

type UnthunkedActionCreator<T> =
  T extends (...args: infer A) => AppThunkAction<any, any, infer R> ? (...args: A) => R : never;

type UnthunkedActionCreators<O extends Record<keyof O, (...args: any[]) => AppThunkAction<any, any, any>>> = {
  [K in keyof O]: UnthunkedActionCreator<O[K]>
}

UnthunkedActionCreator converts a function that returns an AppThunkAction into a function of the same arguments that returns what the AppThunkAction returns. And UnthunkedActionCreators maps UnthunkedActionCreator over an object. Let's see if it works:

type ActionCreatorsType = UnthunkedActionCreators<typeof actionCreators>;

which inspects as

// type ActionCreatorsType = {
//   foo: (baz: string) => Promise<void>;
//   bar: () => Promise<number>;
// }

Looks good. See, all we had to do was wait a year. Cheers!

This is now possible (as of version 3.0), here is the code:

type GetActionCreatorType<FF extends (...args: any[]) => (...args: any[]) => any> =
  FF extends (...args: infer A) => (...args: infer _) => infer R ? (...args: A) => R 
  : never;

type GetActionCreatorsTypes<T extends {[key: string]: (...args: any[]) => (...args: 
  any[]) => any}> = { 
    [P in keyof T]: GetActionCreatorType<T[P]>; 
};

You can use the first function on a single function while the second can be used on object of action creators.

My answer is based on this answer

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM