How can I type a function that optionally has a second parameter if it's event's data is not undefined?
For example:
type Events = {
a: { bc: number }
d: undefined
}
type Fn<T extends Record<string, unknown>> = <E extends keyof T>(event: E, data: T[E]) => void
const fn: Fn<Events> = getFN();
When someone calls fn('a')
Typescript will correctly throw an error because { bc: number }
wasn't supplied. However, fn('d')
will throw an error because typescript is expecting fn('d', undefined)
.
How do I drop the requirement to supply undefined
for fn('d')
?
CodePudding user response:
If a function parameter is optional, then it will accept undefined
as a value. But the converse is not necessarily true; just because a function parameter accepts undefined
it doesn't mean it's optional. If you want an undefined
type for T[E]
to make data
an optional parameter, then your Fn
type needs to be made more complicated; the function parameters themselves need to depend on T[E]
and not just their types.
Here's one way to do it:
type Fn<T extends Record<string, unknown>> = <E extends keyof T>(
event: E, ...rest: undefined extends T[E] ? [data?: T[E]] : [data: T[E]]
) => void
Now the Fn
type is a generic function whose first parameter is of type E
, and after that the array of the rest of the parameters is of a conditional type that depends on T[E]
.
Let's examine that conditional type: undefined extends T[E] ? [data?: T[E]] : [data: T[E]]
. So if the property type T[E]
accepts undefined
, then the rest of the parameters list is a tuple with a single optional element with a label data
. If the property type T[E]
does not accept undefined
, then the rest of the parameters list is a tuple with a single required element, also labeled data
. That means no matter what, we're building a two-parameter function type, but if T[E]
accepts undefined, the second parameter will be optional.
Let's try it out:
declare const fn: Fn<Events>;
fn('a'); // error, expected 2 elements, got 1
/* const fn: <"a">(event: "a", data: { bc: number; }) => void */
fn('a', { bc: 123 }); // okay
fn('d') // okay
/* const fn: <"d">(event: "d", data?: undefined) => void */
That all behaves as expected. When you use IntelliSense to observe the type of fn('a', ...)
, you see that it has two parameters, the second of which is named data
, required, and of type {bc: number}
. When you call fn('d', ...)
, on the other hand, the second parameter is also named data
, but's optional and of type undefined
. So now you get errors where and only where you expect.
If the type passed to Fn
has an optional property, this will automatically become an optional data
parameter of the same type:
declare const o: Fn<{ opt?: string }>
o('opt', "str"); // okay
o('opt'); // also okay
Looks good.