简体   繁体   中英

Can I infer types from input arguments?

Can types be inferred from an options object?

Here is my contrived example.

The idea here is I have a function that takes in an object, where fields are conversion functions (convert A to B, etc). The return of this function is an object with similar functions, but they have a slightly different return type.

I've put some generics in but I'm not really sure how to make it work. I've seen a similar design with Redux Toolkit's createSlice() function, but it's pretty complicated.

type InputActionFn = (arg: any) => any;
interface Options {
    actions: {
        [K: string]: InputActionFn;
    };
}

interface ActionPayload<B> {
    payload: B;
}
type OutputActionFn<A, B> = (arg: A) => ActionPayload<B>;

interface ConverterReturn {
    actions: {
        [K: string]: OutputActionFn<any, any>;
    };
}

export function createConvertor(options: Options): ConverterReturn {
    const actions: Record<string, OutputActionFn<any, any>> = {};

    // For each key I create a function that returns my converted payload.
    Object.keys(options.actions).forEach(key => {
        actions[key] = arg => {
            return {
                payload: options.actions[key](arg),
            };
        };
    });

    return {
        actions,
    };
}

And this is how I plan on using it:

// I can specify as many converter functions here as I want
const conv = createConvertor({
    actions: {
        actionA: (a: number) => a.toFixed(),
        actionB: (b: string) => parseInt(b, 10),
    },
});

// actionA should be typed properly like this:
//   actionA(arg: number) => ActionPayload<string>
const resultA = conv.actions.actionA(5);
console.log(resultA.payload);
// Outputs: "5"

const resultB = conv.actions.actionB('10');
console.log(resultB.payload);
// Outputs: 10

It is possible using typescript to infer the types

Here's how:

type InputActionFn = (arg: any) => any;

interface Options {
  [K: string]: InputActionFn;
}

interface ActionPayload<B> {
  payload: B;
}

type OutputActionFn<A, B> = (arg: A) => ActionPayload<B>;

type Actions<T extends Options> = {
  [key in keyof T]: OutputActionFn<Parameters<T[key]>[0], ReturnType<T[key]>>;
};

export function createConvertor<T extends Options = any>(options: T) {
  const actions = {} as Actions<T>;

  // For each key I create a function that returns my converted payload.
  Object.keys(options).forEach((key) => {
    actions[key] = (arg: Parameters<T[keyof T]>[0]) => {
      return {
        payload: options[key](arg)
      };
    };
  });

  return {
    actions
  };
}

const conv = createConvertor({
  actionA: (a: number) => a.toFixed(),
  actionB: (b: string) => parseInt(b, 10)
});

// Action A is hinted as (arg: number) => ActionPayload<string>
const resultA = conv.actions.actionA(5);
console.log(resultA.payload);
// Outputs: "5"

// Action B is hinted as (arg: string) => ActionPayload<number>
const resultB = conv.actions.actionB('10');
console.log(resultB.payload);
// Outputs: 10

The import part is here

type Actions<T extends Options> = {
  [key in keyof T]: OutputActionFn<Parameters<T[key]>[0], ReturnType<T[key]>>;
};

We're telling TypeScript to type Actions as an object where it's keys are in the keyof of the generic T. For each of those keys we specify that the property type is an OutputActionFn and pass the corresponding types to the Generics. The Parameters<T> Utility Type helps us infer the argument type of the input function you pass (see https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype ). As there's only one expected argument, we pick Parameters<T>[0] as our argument type. Then we use another Utility Type ReturnType<T> (see https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype ) which infers the return type of the input function we pass.

Only problem will be that ESLint will complain about this. Might have to ts-ignore that.

// TS2536: Type 'string' cannot be used to index type 'Actions '.
actions[key] = (arg: Parameters<T[keyof T]>[0]) => {
      return {
        payload: options[key](arg)
      };
    };

Edit: I almost forgot. This is of course also a very important detail that should not be overlooked.

export function createConvertor<T extends Options = any>(options: T) {

The generic Type passed to the createConvertor function is applied on the options argument of the function. We do not use the Options interface, which would not return the expected types. Though options is still typed as an argument of type Options as we extend the generic type T from Options.

Using a couple of TypeScript generic utilities ( in , Parameters , ReturnType , etc.), you can transform object types pretty flexibly.

Here's a working example for your code sample:

type Payloader<T extends Record<string, any>> = {
  [Key in keyof T]: (...params: Parameters<T[Key]>) => ({
    payload: ReturnType<T[Key]>
  })
}

function createConvertor<Actions extends Record<string, any>>(options: { actions: Actions }) {
  const actions: Payloader<Actions> = {} as any;

  Object.keys(options.actions).forEach((key) => 
      // @ts-ignore
      actions[key] = ((...args) => ({
        payload: options.actions[key](...args),
      })
  ))

  return { actions }
}

const conv = createConvertor({
  actions: {
    actionA: (a: number) => a.toFixed(),
    actionB: (b: string) => parseInt(b, 10)
  },
})

const resultA = conv.actions.actionA(5);
console.log(resultA.payload);
// Outputs: "5"

const resultB = conv.actions.actionB('10');
console.log(resultB.payload);
// Outputs: 10

* Note: The function implementation uses @ts-ignore , assuming we don't need the implementation to be typed correctly.

TypeScript Playground

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