Skip to content

Understanding The Intersection In Typescript Might Be Harder Than You Think

Published: at 12:00 AM

At its base, intersection in Typescript is a very easy concept. It combines multiple types into one, right? Well, not -really- though. Let’s find out why.

Reminder: what’s an intersection again?

We take two types and try to “combine” them with an &:

type Person = { name: string };
type Student = { grade: number };

type Combined = Person & Student; // { name: string; grade: number; }

The name intersection comes from set theory

Intersection comes from set theory in math. It takes shared members between sets and creates a new set. That new set is called the intersection.

An example of intersection in set theory

type Hero = { hero: 'Clark Kent' };
type Villain = { villain: 'Lex Luthor' };
type Smallville = Hero & Villain;

How does intersection work in Typescript?

This results in a type that describes a value that can be used as follows:

{ hero: "Clark Kent", villain: "Lex Luthor" };

Unlike the intersection from the set theory example, Hero and Villain do not have shared members. They do not have keys that are available on both types. So how did it achieve its result? What did it intersect? Required properties? Not really. Try and think what the following examples would yield:

type Example1 = 'Clark Kent' & 'Bruce Wayne';
type Example2 = { hero: string; hero: 'Clark Kent' };
type Example3 = 23 & 'Michael Jordan';
type Example4 = { number: 23 } & { number: 8 };
type Example5 = boolean & true;
type Example6 = false & boolean & true;
type Example7 = ('Clark Kent' | 'Bruce Wayne') & ('Lex Luthor' | 'Joker');

Understanding structural typing

In order to understand what intersection is really doing and what it’s intersecting, we need to understand Typescript’s type system. Typescript has a structural type system, which means that types are determined by the structure of the data rather than by the name of the class or interface (which is the case with nominal typing). But this probably means absolutely nothing to you if you’re at this point.

To clarify: you can throw any value against a type, and as long as it adheres to the minimum requirements of the type, it’s valid.

I know, still abstract. Let’s look at an example:

type Movie = { title: string };

function printMovie(movie: Movie) {
  return movie.title;
}

const movie = { title: 'The Dark Knight' };
const hero = printMovie(movie); // ✅

This makes sense. Nothing weird to see here. But let’s say we add an extra property to the anonymous object movie:

const movie = { title: 'The Dark Knight', hero: 'Batman' };
const hero = printMovie(movie); // ✅

This is still valid. And the following works as well:

const movie = {
  title: 'The Dark Knight',
  hero: 'Batman',
  hasGoodEnding: true,
  imdbGrade: 9,
};

const hero = printMovie(movie); // ✅

movie has the title key which has a string value, so it adheres to the Movie type. It doesn’t care about the other properties.

Theres an exception In Typescript, for object literals, called “excess property checking“. We’ll discuss it at the end.

Thinking about types as sets of possible values

If we analyse the previous example we could say that when you create the following type:

type Movie = { title: string };

It describes that any object value can be assigned to it, as long as it has a key value pair of { title: string } at minimum. The following values (a set if you will) are all valid according to type Movie:

const movie = { title: "The Dark Knight" };
const movie = { title: "The Dark Knight", villain: "Joker"};
const movie = { title: "The Dark Knight", year: 2008, imdbUrl: "https://www.imdb.com/title/tt0468569/", hasGoodEnding: boolean; numberOfViewers: 13 };

As you may have deducted: the type Movie describes an infinite amount (a set) of object values that are assignable to the type Movie. We can add thousands of extra keys and it will still be valid.

💡

A type describes a set of possible values


Intersection intersects on possible values that can be assigned to a type

Now that we know that a type describes a set of possible values, we can look at what intersection does. Let’s go back to our initial Smallville type:

type Hero = { hero: 'Clark Kent' };
type Villain = { villain: 'Lex Luthor' };
type Smallville = Hero & Villain;

It’s an intersection that contains two types: Hero and Villain. Hero describes an almost infinite amount of possible values that can be assigned to it:

const hero = { hero: "Clark Kent" };
const hero = { hero: "Clark Kent", age: 19};
const hero = { hero: "Clark Kent", isJournalist: true, movies: ["Man of Steel", "Superman 3"};
// etc, etc.

And it will also describe this possible value:

const hero = { hero: 'Clark Kent', villain: 'Lex Luthor' };

The same goes for Villain:

const villain = { villain: 'Lex Luthor' };
const villain = { villain: 'Lex Luthor', age: 24 };
const villain = { villain: 'Lex Luthor', hair: false, friends: [], father: 'Lionel' };

// But it also describes this possible value:
const villain = { villain: 'lex luthor', hero: 'clark kent' };

And as you can see, it has found a shared possible value between both types:

{ villain: "Lex Luthor", hero: "Clark Kent" }

And that’s where it intersects. Hence the name intersection.

The intersection is the shared possible value between all types

Applying the same principle on other types

Let’s intersect other types than objects and start with a string type.

type Hero = string;

The same goes here; you have to look at the type string as a set of possible values. In this case a set of an infinite amount of literal strings:

type Hero = string;
const printHero = (hero: Hero) => hero;

// These are all valid, because they belong to the set of the `string` type
const hero = 's';
const hero = 'spide';
const hero = 'batm';
const hero = 'aquaman';
// etc, etc

Every string (literal) you can imagine is a part of the possible values within the string set.

Let’s create a string literal type:

type Villain = 'Lex Luthor';

We can look at the Villain type, again, as a set of possible values. But now the only possible value here is ‘Lex Luthor’. It’s a set of one:

const printVillain = (villain: Villain) => villain;

// The only possible value that can be assigned to Villain:
const villain = 'Lex Luthor';

We can’t assign something else than “Lex Luthor” to Villain because we’ve explicitly described that the set of possible values we’re allowing is just one string literal: “Lex Luthor”.

Let’s intersect:

type Hero = string;
type Villain = 'Lex Luthor';
type MovieIntersection = Hero & Villain;

As we saw before, it tries to intersect on possible values described by all types in the intersection. Since Villain is a set of one: ‘Lex Luthor’, and Hero is a set of all strings possible, it will intersect on the ‘Lex Luthor’ string literal.

type Hero = string;
type Villain = 'Lex Luthor';
type MovieIntersection = Hero & Villain; // "Lex Luthor"

Empty intersections (set) will result in a never type

You’ve probably seen the never type around. It represents an empty set of possible values. If an intersection can not find a shared possible value between all passed types, it will result in an empty set: never.

type Hero = 'Clark Kent';
type Villain = 'Lex Luthor';
type Characters = Hero & Villain; // never

type MinAge = 23;
type MaxAge = 50;
type Ages = MinAge & MaxAge; // never

There is no overlapping possible value that can be assigned to both Hero and Villain so the middle part stays empty and the intersection is of type never.

The same goes for objects. For example; two sets that have the same key with a string literal will never have a matching possible value.

type Hero = { name: 'Clark Kent' };
type Villain = { name: 'Lex Luthor' };
type Characters = Hero & Villain; // never

Typescript’s own little exception that could confuse you: excess property checking

We’ve gone over the theory and as to why things are called as they are and how they work. But there’s one important thing to note. Typescript made a very specific deviation from standard implementation of structural typing: annotated object literals will be checked for excess properties.

To illustrate; this is our earlier example:

type Movie = { title: string };
const printMovie = (movie: Movie) => movie.title;

And when we created an anonymous object to pass to printMovie, everything worked as expected:

const movie = { title: "The Dark Knight", hero: "Batman" };
const hero = printMovie(movie); ✅

But when we try to insert an object literal:

const hero = printMovie({
  title: "The Dark Knight",
  hero: "Batman"
}); ❌

Typescript will give us an error:

❌ Object literal may only specify known properties, and ‘hero’ does not exist in type ‘Movie’.(2353)

And that’s because Typescript has a built-in excess property check for object literals. It assumes you have made an error when explicitly passing an object literal as a value that doesn’t exactly adhere to the Movie type.

It’s the same reason why this will fail as well:

type Movie = { title: string };
const movie: Movie = { title: 'Superman', villain: 'Lex Luthor' };

❌ Object literal may only specify known properties, and ‘villain’ does not exist in type ‘Movie’.(2353)

Closing notes

Understanding Typescript’s type system is, I think, the key to mastering the language. Although we have focused on the intersections, you can apply the same learned principles on other parts of Typescript as well.

Any questions? Let me know!