Home > Back-end >  Require argument if unknown type is not undefined
Require argument if unknown type is not undefined

Time:12-16

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.

Playground link to code

  • Related