Optional parameters based on conditional types
Asked Answered
A

4

51

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
Actually answered 13/9, 2018 at 16:28 Comment(2)
Optional parameters are a runtime feature (evaluating & passing the default value), which cannot be affected by the type system.Gamosepalous
You could use a simple overload type instead.Gamosepalous
S
91

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
Silin answered 13/9, 2018 at 16:35 Comment(11)
Also you can use : [] instead of : [undefined?] if you actually want to remove the parameter entirely.Auspex
@ProdigySim, Is there any way to do this with overloads in the way you described?Spoondrift
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.Spoondrift
@Spoondrift Yup, you need to get into the array. This is why simple overloads are usually a better solutionSilin
I'm mostly interested in what @Auspex mentioned in the first comment here, which enables conditionally required arguments. Is there any way to get conditionally required arguments via overloads?Spoondrift
You would just leave the argument off the overload. function foo2(first: number): undefined;Auspex
@Auspex The original request was that undefined can be passed as a second argument, but if it is, the first argument must be number. This was the requesy in the question, this is why I wrote it as such.Silin
When I try the overload solution, I get this error: The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.Description
This dosent work for interfaces. Any way to make this work for interfaces ?Guarnerius
Can someone explain to me why do we need to wrap it up into an array ?Cope
@kidz55. It's a tuple type, so we can have optinality on only one side of the conditional type.Silin
F
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.

Flirt answered 20/2, 2023 at 12:54 Comment(2)
Hey @Flirt 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? ThanksMacgregor
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? #37688818Flirt
G
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

Grantee answered 22/1 at 16:53 Comment(0)
N
0

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
Newsom answered 8/7 at 9:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.