To achieve this specific functionality, it is essential for call()
to support multiple call signatures.
Traditionally, you would have to turn call()
into an overloaded function, but another approach involves accepting a tuple-typed rest parameter.
Here's one way to implement it:
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 type OptionalIfUndefined<T>
is a conditional type that determines if undefined
can be assigned to T
. Depending on the result, it either creates a tuple with an optional element of type T
, or a tuple with a required element of type T
.
In the function definition of call()
, we use the rest parameter type
OptionalIfUndefined<TypeObj[K]>
. By employing
destructuring assignment, we extract the single element from the array since only its value is needed.
If you expect param
to be recognized as type
TypeObj[K]</code within the implementation of <code>call()
, you may face challenges due to the generic conditional type being opaque to the compiler. It’s recommended to
assert the type like so:
const _param = param as TypeObj[K]
, and then utilize
_param
instead of
param</code if necessary.</p>
<p>Let’s test it out:</p>
<pre><code>call("a") // correct
call("a", undefined) // correct
call("b", 1); // correct
call("b") // error
call("c", "a"); // correct
call("c") // error
Everything seems to be functioning as expected!
Access the code on the TypeScript Playground