Reputation: 40104
I have an interface:
interface HandlerEvent<T = void> {
data: T // I would like to keep this as a required parameter
}
I would like to be able to make data
required if I pass a value when calling it, else make it optional.
const actionHandler = ({}: HandlerEvent):void => { }
actionHandler({}) // Fails as data is required. How to get rid of error without specifying data?
actionHandler({data: undefined }) // A bit silly imo
So that when no parameters are provided to the generic I can call the function without it asking for data
and if I do provide T
then it will require it. I can of course do data?: T
but I'd like to find an alternative to checking the presence of data
in code. I thought the default parameter (void
in this case) would work but it still requires me to pass data
.
Right now I cannot call: actionHandler()
without errors even when I do not need data. Perhaps I am overlooking what to pass to the generic?
I've found a few related discussions but I don't see a solution - they just are closed though perhaps I missed it deep in the comments.
And How to pass optional parameters while omitting some other optional parameters?
I did find a post that wraps the generic - is there a simpler way that I am overlooking?
TS Playground link
Upvotes: 1
Views: 1569
Reputation: 328503
You can use a conditional type to make data
optional if and only if undefined
is assignable to T
:
type HandlerEvent<T = void> = undefined extends T ? { data?: T } : { data: T }
This then behaves as you desire:
type H = HandlerEvent
// type H = { data?: void | undefined; }
const actionHandler = (x: HandlerEvent): void => { }
actionHandler({}); // okay
actionHandler({ data: undefined }) // okay
If, as in your followup comment, you need to do this sort of thing for a bunch of properties, you can write an UndefinedToOptional<T>
type function which takes an object type T
, and makes optional any property for which undefined
is assignable to it.
Unfortunately the type function is kind of icky looking, especially if you want to write it so that the properties come out in the same order that they went in (not that property order matters for anything but documentation):
type UndefinedToOptional<T> = { [K in keyof T]-?:
(x: undefined extends T[K] ? { [P in K]?: T[K] } : { [P in K]: T[K] }) => void
}[keyof T] extends (x: infer I) => void ?
I extends infer U ? { [K in keyof U]: U[K] } : never : never
The idea here is to iterate over each property key K
from the keys of T
, and make a type for each of them consisting of just that property. If undefined extends T[K]
then we make it optional; otherwise we leave it alone. So {a: 0, b: 1 | undefined}
would turn into the types {a: 0}
and {b?: 1 | undefined}
. Then we get the union of all those, transform that union into an intersection, and then merge that intersection into a single object type. It's ugly, but it works, at least for object types without index signatures or call signatures:
type Test = UndefinedToOptional<
{ a: string, b: number | undefined, c: unknown, d: boolean, e: any }
>;
// type Test = {
// a: string; b?: number | undefined; c?: unknown; d: boolean; e?: any;
// }
And then you could define HandlerEvent
like this:
type HandlerEvent<T = void> = UndefinedToOptional<{ data: T }>
which resolves to the same thing.
Upvotes: 4