20

I'll try my best in describing our case, as I have no idea how to title this.

We have a package, which handles requesting our services/cloud functions (if for example service A needs something done by service B, it uses this package). This package exposes a function to call each service (for example for service auth, there would be a auth function exposed. We have a definition of available endpoints for each service and as this grows, it's harder to maintain since we were unable to figure out a way to type the parameters for the requests.

What I wanted to do, is simply have another type which would contain the parameter types for each endpoint of each service. For example we have endpoint called getEmail, which would return email of a user and requires an id parameter. Type params type would look like this:

type Params = {
  getEmail: {
    id: number;
  }
}

Very simplified version of the code:

type FunctionName = 'foo' | 'bar' | 'baz';

type FunctionParams = {
  foo: {
    myVar: number;
  };
  bar: {
    myVar: string;
  };
  baz: Record<string, number>;
};

declare const sendRequest: (...args: any[]) => any;

const callFunction = (
  fn: FunctionName,
  params: FunctionParams[typeof fn],
  //                     ^^^^^^^^^ What should I put here?
) => {
  sendRequest(fn, params);
};

callFunction('foo', {
  myVar: 'this should fail',
  // This is allowed, as the type of the second parameter is:
  // { myVar: number; } | { myVar: string; } | Record<string, number>) => void
  // I want it to be { myVar: number; }
});

2 Answers 2

28

You want to use generics! You can parameterize callFunction to bind fn to a specific string, and then use this to index FunctionParams:

function callFunction<T extends FunctionName>(
  fn: T,
  params: FunctionParams[T],
) {
  sendRequest(fn, params);
};

callFunction('foo', {
  myVar: 'this should fail', //BOOM!
});

Also, don't waste energy maintaining types. FunctionName can be as easy as

type FunctionName = keyof FunctionParams

with that, every time you add new params, FunctionName gets updated. See also this playground

Sign up to request clarification or add additional context in comments.

2 Comments

Generics did come to mind, we already use one for the return type but I had no idea typescript can automatically infer the generic, I thought they had to be either required or have a default value. This actually solves our problem nicely, it will just require quite a lot of time. Thanks a lot!
Thanks, @ddprrt, the example is clear! This way, the function call is type-safe. But how would you achieve it when defining a function as a callback? So that it is clear when implementing it that if fn is 'foo', params will be { bar: number }. E.g.: callback <T extends FunctionName>(fn: T, params: FunctionParams[T]) => { if (fn === 'foo' ) console.log(params.bar); // OK, will print some string }
1

May be, you will need other kind of implementation, like following:


/**
 * API Functions Mapping.
 */
interface ApiFunctions {
  foo: {
    myVar: number;
  };
  bar: {
    myVar: string;
  };
  baz: Record<string, number>;
}

/**
 * Generic API Functions method.
 *
 * @param args
 * @returns
 */
declare function sendRequest(...args: any[]): any;

/**
 * Call {@link sendRequest} wrapper.
 *
 * @param fn
 * @param params
 */
function callFunction<T extends keyof ApiFunctions>(
  fn: T,
  params: ApiFunctions[typeof fn]
) {
  sendRequest(fn, params);
}

callFunction('foo', {
  myVar: 'this should FAIL', // <<<--- FAIL!
});

callFunction('bar', {
  myVar: 'this should WORK',
});

Probably the following article can help you: https://typeofnan.dev/how-to-make-one-function-argument-dependent-on-another-in-typescript/

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.