Avatar

Nǐ hǎo, I'm Julia.

Using Typescript Unions With Never and Extends

#typescript

3 min read

I've reached a point where I'm now really enjoying working with Typescript. I remember one of the senior engineers in my team telling me when I first started that I would find myself being a Typescript convert one day. I didn't really believe it then, as TS seemed to be so much more of a hindrance than help in the early days (it wouldn't even allow me to run my code to debug it!)...but I'm happy to say he was right. ✌🏻

I've reached the tipping point, where it now feels weird to NOT have Typescript switched on for a Javascript repo. There are, for sure, still days when I'm trying to get a specific scenario typed properly and I'm finding it difficult to even know what to Google for... but it gets easier over time as you start to build a bank of knowledge and realise that the same patterns keep cropping up again and again.

One pattern that I keep coming across is this one - let's say I have a Media component that I need to pass certain props into, for example:

{
  id: number;
  group: string;
  bookTitle: string;
  ebook: boolean;
  tvShowTitle: string;
  platform: string;
  songTitle: string;
  artist: string;
}

There are certain props that always needs to be present for the Media component to render, with the rest being conditional on whether certain other props are present:

// Always required
{
  id: number;
  group: string;
}

// Also need to have either Option 1 or 2 present.

// Option 1: Both required, or neither present
{
  bookTitle: string;
  ebook: boolean;
}

// Option 2: Both required, or neither present
{
  tvShowTitle: string;
  platform: string;
}

How do we ask Typescript to check that if say bookTitle is passed as a prop, ebook becomes a required prop, whereas tvShowTitle and platform should NOT be present? Additionally, we want to let Typescript know that id and group should ALWAYS be required.

The answer is through a combination of union types and extends.

interface MediaBase {
  id: string;
  group: string;
}

interface BookProps extends MediaBase {
  bookTitle: string;
  ebook: boolean;
  tvShowTitle: never;
  platform: never;
}

interface TvShowProps extends MediaBase {
  bookTitle: never;
  ebook: never;
  tvShowTitle: string;
  platform: string;
}

export type MediaProps = BookProps | TvShowProps;

MediaProps here is the final type of the Media component.

  • This can either take the form of BookProps or TvShowProps (union type).
  • We are explicit in our BookProps and TvShowProps using never to tell Typescript that these props should not be passed in if certain other props are present.
  • By using extends MediaBase, both BookProps and TvShowProps also has id and group as required props.

© 2016-2024 Julia Tan · Powered by Next JS.