1

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

cyberwombat
  • 31,246
  • 30
  • 143
  • 210
  • "call the function"... which function? It helps if the code is a [mcve] which, when dropped into a standalone IDE like the [TypeScript Playground](https://www.typescriptlang.org/play), demonstrates what you're talking about. – jcalz May 20 '21 at 16:19
  • `actionHandler()` – cyberwombat May 20 '21 at 16:33
  • 1
    But that’s not a function, at least not as written. Can you demonstrate with some well-formed code what the issue you’re facing is? Preferably something that would show why just making `data` an optional property in `HandlerEvent` doesn’t suffice. – jcalz May 20 '21 at 16:37
  • I added a playground. If I make `data` optional then, in the code I have to ensure that data is present. Since I have the option to pass parameters to the generic I was wondering if there was a way to have a default value. For example if I provide T = void (or undefined) then I have to still provide `data` but I must explicitly specify it as undefined which seems a bit ridiculous. – cyberwombat May 20 '21 at 16:54
  • To summarize - I would like data to be required if I pass T else not required - i.e. undefined – cyberwombat May 20 '21 at 17:04
  • So, you want [this](https://tsplay.dev/WvpXMN) maybe? Can you make sure this works for your use cases? If so, I'm happy to write up an answer. If not, please modify the example code to clarify what's lacking. – jcalz May 20 '21 at 19:03
  • @jcalz that is exactly what I needed! – cyberwombat May 21 '21 at 02:09
  • If there were more than one field like `data` though and I wanted to apply the same logic... is there a way to make it more manageable? Say data1,data2,data3... each of which should be required if T1,T2,T3 for example is not void, optional otherwise. – cyberwombat May 21 '21 at 02:15

1 Answers1

1

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.

Playground link to code

jcalz
  • 125,133
  • 11
  • 145
  • 170