Getting union members with specified field in TypeScript

Piotr Pliszko

Recently I've been working on a big refactoring where I've needed to revisit some types we have previously defined. Because we've now decided to go with more granular types, I had to prepare type guards to narrow down the types in some shared utilities. I've found myself in a situation where I needed to get a subset of a union type based on a specified field. Let's see how to do it.

The problem

This is simplified situation but should show the point. Let's say we have these three types and their union:

interface Song {
  title: string;
  artist: string;
  album: string;
  year: number;
  duration: number;
}

interface Movie {
  title: string;
  director: string;
  year: number;
  duration: number;
}

interface Book {
  title: string;
  author: string;
  year: number;
}

type Media = Song | Movie | Book;

We may not want to specify a common interface they will all implement, because they are not that closely related, but we want to have a type that will contain only the types that have a specified field. For example, we want to have a type that will contain only the types that have a duration field to use in duration-related utils. We would like to be able to do something like this:

type Media = Song | Movie | Book;

// MediaWithDuration should be Song | Movie
type MediaWithDuration = TypesWithField<Media, 'duration'>;

// some util that expects only items with duration
const calculateRequiredTime = (mediaItems: MediaWithDuration[]): number => {
  // ...
};

// type guard that uses this type
const isMediaWithDuration = (media: Media): media is MediaWithDuration => {
  return media && 'duration' in media;
};

The solution

So, how can we filter out the types that don't have a duration field? Let's use some TypeScript type mechanics to do it. First, let's define TypesWithField type:

type TypesWithField

We need to pass two parameters to it - U will be our union type, and F will be the field name we want to look for.

type TypesWithField<U, F>

Now, how we can check if a type has a specified field? There is a nice mechanism that will allow us to do so:

F extends keyof U ? U : never

In the code above, we check if F (which is a string representing a key) is among the keys of U. If it is, we return U, if not, we return never. So, what will happen if we try doing something like this?

type TypesWithField<U, F> = F extends keyof U ? U : never;

type MediaWithDuration = TypesWithField<Media, 'duration'>;
// type MediaWithDuration = never

But why is it never? It's because we have passed the whole union as U, and we should check every union member separately. So, how we can easily do this? There is a simple trick for this:

U extends U

It seems like a tautological statement, but this is a nice trick to distribute union type members in the conditional. If we use this, we will be able to check the rest of the statement on each member separately. Let's glue this together then:

type TypesWithField<U, F> = U extends U ? (F extends keyof U ? U : never) : never;

Now, if we try to use it, we will get the expected result:

type TypesWithField<U, F> = U extends U ? (F extends keyof U ? U : never) : never;

type MediaWithDuration = TypesWithField<Media, 'duration'>;
// type MediaWithDuration = Song | Movie

This is it!

If you have any questions or feedback, feel free to reach out to me on GitHub, Twitter/X or LinkedIn!