Tristan F.-R.
Tristan F.-R.

Reputation: 4224

How can I make an undefined parameter not required?

Playground

I'm trying to make undefined values be optional, while non-optional values are optional. My perfect example would be:

type TypeObj = {
  a: undefined;
  b: number;
  c: string;
}

call("a"); call("b", 1); call("c", "3"); // should work
call("b", "a"); call("c", 3) // shouldn't work
call("b"); call("c") // also shouldn't work

but my current approach (with the second parameter being optional) allows calling b and c without the second parameter, which I do not want.

function call<K extends keyof TypeObj = keyof TypeObj>(key: K, param?: TypeObj[K]) {

}

call("a"); call("b", 1); call("c", "3"); // works (good)
call("b", "a"); call("c", 3) // doesn't work (good)
call("b"); call("c") // works (don't want that)

Upvotes: 2

Views: 2705

Answers (1)

jcalz
jcalz

Reputation: 330161

This sort of thing essentially requires call() to have multiple call signatures. Traditionally this would need you to make call() an overloaded function, but you can also do it by having the function accept a tuple-typed rest parameter.

Here's one approach:

type OptionalIfUndefined<T> =
  undefined extends T ? [param?: T] : [param: T];

function call<K extends keyof TypeObj>(
  key: K, ...[param]: OptionalIfUndefined<TypeObj[K]>
) {
  const _param = param as TypeObj[K]; // <-- might need this
}

The OptionalIfUndefined<T> type is a conditional type that checks if undefined is assignable to T. If so, it evaluates to a single-element tuple with an optional element of type T; otherwise, it evaluates to a single-element tuple with a required element of type T.

Then call() is given a rest parameter of type OptionalIfUndefined<TypeObj[K]>. We use destructuring assignment so that param is a variable name holding the single element, since that's all we really want (the actual array holding it isn't useful to us).

In your example you didn't do anything inside the implementation of call(), but if you were expecting param to be known as type TypeObj[K] you'll be disappointed. A generic conditional type is essentially opaque to the compiler, so it doesn't know that typeof param will be anything narrower than TypeObj[K] | undefined. You'll need to assert it like const _param = param as TypeObj[K] and then use _param instead of param (or swap the names if you want) if you need such functionality.

Well, let's make sure it works:

call("a") // okay
call("a", undefined) // okay
call("b", 1); // okay
call("b") // error
call("c", "a"); // okay
call("c") // error

Looks good!

Playground link to code

Upvotes: 4

Related Questions