Optional parameters based on conditional types

25.8k Views Asked by At

Is it possible to make a function have either mandatory or optional parameters based on conditional types in TypeScript?

This is what I've got so far:

const foo = <T extends string | number>(
    first: T,
    second: T extends string ? boolean : undefined
) => undefined;

foo('foo', true); // ok, as intended
foo(2, true); // not ok, as intended
foo(2, undefined); // ok, as intended
foo(2); // compiler error! I want this to be ok
3

There are 3 best solutions below

11
On BEST ANSWER

You can do this in 3.1 using Tuples in rest parameters and spread expressions

const foo = <T extends string | number>(
  first: T, 
  ...a: (T extends string ? [boolean] : [undefined?])
) => undefined;

foo('foo', true); // ok, as intended
foo(2, true); // not ok, as intended
foo(2, undefined); // ok, as intended
foo(2); // ok

But the better way is to use overloads.

function foo2(first: string, second: boolean) : undefined
function foo2(first: number, second?: undefined): undefined
function foo2<T>(first: T, second?: boolean): undefined{
  return undefined
}

foo2('foo', true); // ok, as intended
foo2(2, true); // not ok, as intended
foo2(2, undefined); // ok, as intended
foo2(2); // ok
2
On

To ensure a second argument is never provided (even when undefined), you could group both parameters into the the rest statement.

const bar = <T extends string | number>(
  ...args: (T extends string ? [T, boolean] : [T])
) => undefined;

// Usage

bar('bar', true); // ok, as intended
bar(2, true); // not ok, as intended
bar(2); // ok, as intended
bar(2, undefined); // not ok

This is a small adjunct to @titian-cernicova-dragomir's answer.

Demo.

0
On

Building on the answer of @robstarbuck, to avoid having to consume the arguments as spread arguments, you can use overloading or simply type overriding. As long as the implementation matches the overloaded types, TS seems to use the strictest types it can match.

const bar: (<T extends string | number>(
  ...args: (T extends string
    ?  // two arguments
    [T, boolean]
    : // one argument or undefined second argument
    [T] | [T, undefined])
) => string) =
  // implementation without spread args
  <T,>(a: T, b?: boolean) => `${a} ${b}`;

bar('bar', true);  // ok
bar(2, true);      // not ok
bar(2);            // ok
bar(2, undefined); // ok

Demo