torkel
torkel

Reputation: 2476

Optional parameters based on conditional types

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

Upvotes: 53

Views: 29944

Answers (4)

Elyasaf755
Elyasaf755

Reputation: 3519

Previous answers either did not really answer the question or were very messy, I decided to simplify things.

You need to create a conditional type for the function args like so:

type FooArgs<T extends string | number> = T extends string ?
    [first: T, second: boolean] :
    [first: T, second?: undefined];
    
const foo = <T extends string | number>(...args: FooArgs<T>) => {
    const [first, second] = args;
    // do something
};

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

Upvotes: 0

w00t
w00t

Reputation: 18281

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

Upvotes: 0

robstarbuck
robstarbuck

Reputation: 8091

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.

Upvotes: 4

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249586

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

Upvotes: 94

Related Questions