Ryan Killeen

AI finishes my blog post

Published

Foreward

The one in-progress post on my site is about conditional types in Typescript, a super useful feature of the language but a less useful, half-complete article.

After exploring the hot new ChatGPT AI model over at chat.openai.com, I realized it's perfectly primed to finish my blog post. The results are impressive and reproduced below, unedited with the AI's code examples provided. The only prompt it was given?

Write a technical blog post about conditional types in Typescript. Use Panera's You Pick Two menu as an example of conditional types. Follow up with a practical example: abstracting react-select's Creatable, Async, and AsyncCreatable components

Overall, I'm extremely impressed with the quality of the blog post that the AI was able to produce. It's clear that AI has come a long way in its ability to understand and explain complex topics, and I'm excited to see what it will be capable of in the future.*

*It wrote this paragraph too.

Unleashing the Power of Conditional Types in Typescript: A Practical Guide

Conditional types in Typescript provide a way to express conditional relationships between types. This allows for more powerful type checking and inference within our code. One common use case for conditional types is modeling complex conditional relationships within our data structures.

For example, consider Panera's "You Pick Two" menu. This menu allows customers to select two items from a list of options, which could include a sandwich, a salad, a soup, or a drink. We can model this menu using a conditional type in Typescript.

First, let's define the base types for our menu items:

type Sandwich = {
  type: 'sandwich';
  name: string;
};

type Salad = {
  type: 'salad';
  name: string;
};

type Soup = {
  type: 'soup';
  name: string;
};

type Drink = {
  type: 'drink';
  name: string;
};

Now, we can use a union type to represent the set of all possible menu items:

type MenuItem = Sandwich | Salad | Soup | Drink;

Next, we can define a type to represent a "You Pick Two" selection. This type should include two properties: item1 and item2, each of which should be a MenuItem.

We can use a conditional type to express the relationship between these two properties. Specifically, we want to ensure that the item1 and item2 properties are never the same type. We can do this using the Exclude type.

The Exclude type takes a union type as its first generic parameter and a type as its second generic parameter. It returns a new union type that excludes the second generic parameter from the first. In our case, we want to exclude the type of item1 from the possible types of item2, and vice versa.

Here's how we would define the YouPickTwo type using a conditional type:

type YouPickTwo = {
  item1: Exclude<MenuItem, typeof item2>;
  item2: Exclude<MenuItem, typeof item1>;
};

Now, when we use the YouPickTwo type, Typescript will enforce the constraint that the item1 and item2 properties must be of different types. For example, the following code would be considered valid:

const selection: YouPickTwo = {
  item1: {
    type: 'sandwich',
    name: 'Turkey and Swiss'
  },
  item2: {
    type: 'soup',
    name: 'Broccoli Cheddar'
  }
};

On the other hand, the following code would generate a compile-time error, because the item1 and item2 properties are both of type Sandwich:

const invalidSelection: YouPickTwo = {
  item1: {
    type: 'sandwich',
    name: 'Turkey and Swiss'
  },
  item2: {
    type: 'sandwich',
    name: 'Roast Beef and Cheddar'
  }
};

In this way, conditional types in Typescript can be used to express complex relationships between types.

To continue with the example, let's consider how we could use conditional types to abstract the differences between the Creatable, Async, and AsyncCreatable components in the react-select library.

These three components have a lot in common, but there are also some key differences between them. The Creatable component allows users to create new options, while the Async and AsyncCreatable components fetch options asynchronously. Additionally, the AsyncCreatable component combines the features of both the Creatable and Async components.

We can model these differences using a conditional type. First, let's define a base type for the common properties shared by all three components. This type will include a options property, which will be an array of options that the user can select from:

type SelectProps<OptionType> = {
  options: OptionType[];
};

Next, we can define three new types that extend the SelectProps type with the additional properties needed by each of the three components. The CreatableProps type will include a onCreateOption callback, which will be called when the user creates a new option:

type CreatableProps<OptionType> = SelectProps<OptionType> & {
  onCreateOption: (option: OptionType) => void;
};

The AsyncProps type will include a loadOptions callback, which will be used to fetch options asynchronously:

type AsyncProps<OptionType> = SelectProps<OptionType> & {
  loadOptions: (inputValue: string) => Promise<OptionType[]>;
};

Finally, the AsyncCreatableProps type will combine the features of both the CreatableProps and AsyncProps types:

type AsyncCreatableProps<OptionType> =
  CreatableProps<OptionType> & AsyncProps<OptionType>;

Now, we can use a conditional type to define a single ReactSelectProps type that can be used for all three components. This type will take a generic parameter, IsAsync, which will be a boolean value indicating whether the loadOptions callback should be included in the resulting type.

If IsAsync is true, the ReactSelectProps type will include the loadOptions callback and will extend the AsyncProps type. Otherwise, it will not include the loadOptions callback and will extend the SelectProps type:

type ReactSelectProps<OptionType, IsAsync extends boolean> =
  IsAsync extends true ? AsyncProps<OptionType> : SelectProps<OptionType>;

We can then use the ReactSelectProps type to define a generic ReactSelect component, which will take the IsAsync parameter as a generic type argument:

function ReactSelect<OptionType, IsAsync extends boolean>(
  props: ReactSelectProps<OptionType, IsAsync>
) {
  // implementation details omitted
}

Now, we can use the ReactSelect component to create specific components for each of the three react-select components.