aspirisen
aspirisen

Reputation: 1025

TypeScript: How to omit second argument in function depending on the first argument

I want not to have a second argument in function depending on the what first argument function returns. I can set that its type is undefined but I still need to pass second argument. Sandbox

declare class Params<T> {
  private params: T;
}

interface Data {
  one: string;
  two: {
    three: string;
    four: Params<{ more: string }>;
  };
}

type MyFunction = <T>(
  g: (d: Data) => T,
  args: T extends Params<infer Args> ? Args : undefined
) => T extends Params<{}> ? string : T extends string ? T : unknown;

let mf = (() => {}) as MyFunction;
let a = mf(p => p.one); // expect not to have second arg
var b = mf(p => p.two); // b is unknown (correct now)
var b = mf(p => p.two.four,); // second arg has "more" property

Also there is an issue that in example b I have a tooltip that I can pass second argument but when I do it TS shows error. TS Error And when I provide the argument it shows the error enter image description here

Upvotes: 0

Views: 2050

Answers (1)

jcalz
jcalz

Reputation: 327934

If you want the function to take either one or two parameters depending on a generic type, you could use rest parameters as tuples to express this. Like this:

type MyFunction = <T>(
  g: (d: Data) => T,
  ...args: T extends Params<infer Args> ? [Args] : []
) => T extends Params<{}> ? string : T extends string ? T : unknown;

That's similar to what you had, except that now args is a rest parameter, and it's either one or zero elements long depending on what T is. Now this works:

let mf = (() => {}) as MyFunction;
let a = mf(p => p.one);
var b = mf(p => p.two);
var c = mf((p: Data) => p.two.four, { more: "ads" });

But note that for c I had to explicitly annotate p as Data. Without it, the compiler gets confused and infers any, and then {more: "ads"} is an error (because it couldn't tell that p.two.four was of a Params<> type.


What I'd probably do instead, though, is use overloads to distinguish the two quite different ways of calling this function:

type MyFunction = {
  <A>(g: (d: Data) => Params<A>, args: A): string;
  <T>(g: (d: Data) => T): T extends string ? T : unknown;
};

That's a lot less type manipulation (and cuts out most of your conditional types) and now the compiler really knows what to expect:

let mf = ((() => {}) as any) as MyFunction;
let a = mf(p => p.one);
var b = mf(p => p.two);
var c = mf(p => p.two.four, { more: "ads" });

All of that works perfectly. Okay, hope that helps. Good luck!

Link to code

Upvotes: 2

Related Questions