Passing at least one optional prop with TypeScript

Setting if a prop is optional or not in TypeScript is really simple. If it's optional, add a question mark after the prop name.

interface Fruit {
  apples: string;
  oranges?: string;
 }

But what if you have several optional props, you don't care which one is passed, but one of them has to? That's where Generics can save the day.

interface FruitOptions {
  apple: string;
  orange: number;
  pear?: boolean;
  banana?: string;
  grape?: string[];
}

In this example, I have two props you have to pass, but three optional ones. You have to give me apple and orange, but I'll be pretty full after that so I don't mind if I get a pear, banana, or grapes.

Let's use a Generic help us out.

type AtLeastOneOptionalFruit<T, U = {}, V = {}, W = {}> = T & (U | V | W)

Woah! Yes, it looks daunting, but let's break it down.

The AtLeastOneOptionalFruit type is a generic type that takes four type parameters: T, U, V, and W.  T simply stands for Type. The type parameter represents the required props (apple and orange), and the U, V, and W type parameters represent the optional fruit props. It's generally considered best practice to just move up the alphabet for consecutive variables.

The AtLeastOneOptionalFruit type is defined as a union of the T type and a union of the U, V, and W types, which means that a value of this type must have at least one of the optional fruit props.

Let's put it together in an example.

interface FruitOptions {
  apple: string;
  orange: number;
  pear?: boolean;
  banana?: string;
  grape?: string[];
}

type AtLeastOneOptionalFruit<T, U = {}, V = {}, W = {}> = T & (U | V | W);

function example(options: AtLeastOneOptionalFruit<FruitOptions, { pear: boolean }, { banana: string }, { grape: string[] }>) {
  // do something with the options object
}

// These calls are all valid because they pass at least one of the optional fruit props
example({ apple: 'red', orange: 4, pear: true });
example({ apple: 'red', orange: 4, banana: 'yellow' });
example({ apple: 'red', orange: 4, grape: ['white', 'red'] });
example({ apple: 'red', orange: 4, pear: true, banana: 'yellow' });
example({ apple: 'red', orange: 4, pear: true, grape: ['orange', 'yellow'] });

// This call is invalid because it doesn't pass any of the optional fruit props
example({ apple: 'red', orange: 4 });

This might seem a bit arbitrary, but I recently came across a need for exactly this when writing a custom tooltip that required either a label as a string, or a custom trigger entirely. I didn't want to set either of them as required. Either option was fine, but you had to provide one of them.

The above example isn't very scalable though. What if you want to add more optional props, and don't care which one? Imagine are building a car. We have lots of options to choose from, and many are optional. Do we want heated seats? Cruise control?  

interface CarOptions {
  make?: string;
  model?: string;
  color?: string;
  engineSize?: number;
  transmission?: 'automatic' | 'manual';
  sunroof?: boolean;
  navigation?: boolean;
  heatedSeats?: boolean;
  cruiseControl?: boolean;
  // etc.
}

We can scale our type to account for any number of options using an index signature.

type AtLeastOneCarOption<T> = { [K in keyof T]?: T[K] } & { [K in keyof T]: T[K] }

The AtLeastOneCarOption type is defined as a type intersection, using the & operator, between the T type (just like before) and an object type that has a index signature. The index signature is defined using the [K in keyof T] syntax (K stands for Key), which means that the object type has a string index signature that can be any of the keys of the T type. The type of the value of the index signature is set to be optional, using the ? operator, and is set to be the same as the type of the value of the corresponding key of the T type.

This means that the AtLeastOneCarOption type is a type that has all of the props of the T type, and at least one of the optional props of the T type.

type AtLeastOneCarOption<T> = { [K in keyof T]?: T[K] } & { [K in keyof T]: T[K] }

function purchaseCar(options: AtLeastOneCarOption<CarOptions>) {
  // do something with the options object
}

The AtLeastOneCarOption type is a special type that combines two things: all of the options that we have for the car, and at least one of the optional options that we have for the car. This means that when you use the AtLeastOneCarOption type, you have to choose at least one of the optional options, but you can also choose any of the other options that you want.

Looking at the AtLeastOneCarOption type, the index signature is defined using the [K in keyof T] syntax (K standing for Key), which means that the object type has a string index signature that can be any of the keys of the T type (Type, just like before). The type of the value of the index signature is set to be optional, using the ? operator, and is set to be the same as the type of the value of the corresponding key of the T type. This means that the AtLeastOneCarOption type is a type that has all of the optional props of the T type, and at least one of the optional props of the T type.

The AtLeastOneCarOption type is also defined as a type intersection, using the & operator, between the object type with the index signature and the T type. This means that AtLeastOneCarOption is a type that has all of the props of the T type, and at least one of the optional props of the T type.

This allows the purchaseCar function to accept an object that has any combination of the optional props, as long as at least one of them is present. If the object passed to the purchaseCar function doesn't have any of the optional props, the TypeScript compiler will give an error.

// These calls are also valid because they pass at least one of the optional car options
purchaseCar({ color: 'red' });
purchaseCar({ engineSize: 4.0 });
purchaseCar({ transmission: 'automatic' });
purchaseCar({ color: 'red', engineSize: 4.0 });

// This call is invalid because it doesn't pass any of the optional car options
purchaseCar({});