typescriptgenericstype-inference

Typesafe nested function return using generics


I have a the following mapper with the type associated with, but I have no clue what to look, to type func correctly.

type Action<T, K> = {
  key: K;
  func: // What should this be?
};

type ActionMapper<T> = {
  [K in keyof T]: Action<T, K>;
}; 

export enum EActionSelectPot {
  CREATE_ORDER = 'CREATE_ORDER',
  ADD_ITEM = 'ADD_ITEM',
  DELETE_ORDER = 'DELETE_ORDER',
}

const first_function = () => 'hi';
const second_function = () => ['hi'];
const third_function = () => {
  hi: 'hi';
};

const test: ActionMapper<typeof EActionSelectPot> = {
  CREATE_ORDER: {
    key: EActionSelectPot.CREATE_ORDER,
    func: first_function,
  },
  ADD_ITEM: {
    key: EActionSelectPot.ADD_ITEM,
    func: second_function,
  },
  DELETE_ORDER: {
    key: EActionSelectPot.DELETE_ORDER,
    func: third_function,
  },
};

const hi = test.CREATE_ORDER.func();

My goal is pretty straightforward, I have to have an object that infer the type of the function with generics. The problem is that I have no clue if this is possible at all, if so, is there any way to accomplish such thing.

I've tried with func: () => ReturnType[K]['func']>;, but I really don't know where to look for.

I don't want to infer manually the type of the return, can it be fully dynamic ? Where to look for ?


Solution

  • The generic ActionMapper<T> type you're looking for is

    type ActionMapper<T> = {
      [K in keyof T]: Action<K, T[K]>;
    };
    

    where Action is

    type Action<K, V> = {
      key: K;
      func: () => V;
    };
        
    

    But, unfortunately, you can't ask the compiler to infer T for you in generic types. That means there's no way to write something like:

    // don't do this, it won't work:
    const test: ActionMapper<infer> = {
      [EActionSelectPot.CREATE_ORDER]: {
        key: EActionSelectPot.CREATE_ORDER,
        func: first_function,
      },
      [EActionSelectPot.ADD_ITEM]: {
        key: EActionSelectPot.ADD_ITEM,
        func: second_function,
      },
      [EActionSelectPot.DELETE_ORDER]: {
        key: EActionSelectPot.DELETE_ORDER,
        func: third_function,
      },
    };
    

    and have T be inferred as if you had written

    const test: ActionMapper<{
      [EActionSelectPot.CREATE_ORDER]: string,
      [EActionSelectPot.ADD_ITEM]: string[],
      [EActionSelectPot.DELETE_ORDER]: { hi: string }
    }> = { ⋯ };
    

    There is a longstanding open feature request for this at microsoft/TypeScript#32794, but for now it's not part of the language. You have to work around it.


    The standard workaround is to use generic functions to help. TypeScript will infer type arguments when you call generic functions, so you can make a function whose sole purpose is to give you that inference:

    const actionMapper = <T,>(a: ActionMapper<T>) => a;
    

    That's an identity function at runtime, it's just a => a, and so const test = actionMapper(xxx) and const test = xxx are the same at runtime. But TypeScript can use actionMapper(xxx) to guide the inference for xxx and thus for test:

    const test = actionMapper({
      [EActionSelectPot.CREATE_ORDER]: {
        key: EActionSelectPot.CREATE_ORDER,
        func: first_function,
      },
      [EActionSelectPot.ADD_ITEM]: {
        key: EActionSelectPot.ADD_ITEM,
        func: second_function,
      },
      [EActionSelectPot.DELETE_ORDER]: {
        key: EActionSelectPot.DELETE_ORDER,
        func: third_function,
      },
    })
    test
    //^? const test: ActionMapper<{
    //    [EActionSelectPot.CREATE_ORDER]: string,
    //    [EActionSelectPot.ADD_ITEM]: string[],
    //    [EActionSelectPot.DELETE_ORDER]: { hi: string }
    //   }>
    

    That's the right type, and it was inferred for you, as desired. And while the actionMapper function is a workaround, it's not really much different for the developer from what you wanted. Instead of const test: ActionMapper<infer> = { ⋯ } they write const test = actionMapper(⋯).

    Playground link to code