57

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
2
  • 1
    Optional parameters are a runtime feature (evaluating & passing the default value), which cannot be affected by the type system. Commented Sep 13, 2018 at 16:31
  • You could use a simple overload type instead. Commented Sep 13, 2018 at 16:34

4 Answers 4

99

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
Sign up to request clarification or add additional context in comments.

11 Comments

Also you can use : [] instead of : [undefined?] if you actually want to remove the parameter entirely.
@ProdigySim, Is there any way to do this with overloads in the way you described?
It's important to note that, when using the solution from the first half of this answer, the a parameter is wrapped in an array, which means you need to re-assign via a[0] or use with ...a.
@Julien Yup, you need to get into the array. This is why simple overloads are usually a better solution
Can someone explain to me why do we need to wrap it up into an array ?
|
4

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.

2 Comments

Hey @robstarbuck How about the params object? Here is my code based on your answer. It is fine but the code is not clean tsplay.dev/mALOQN Could you help me to refactor it? Thanks
Hi, can you explain what you're trying to achieve here? My answer has to do with optional parameters not optional object keys for a single parameter, which is I think what you're looking for? This might be a help? stackoverflow.com/questions/37688318/…
1

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

Comments

0

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

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.