cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Intersection types

Combine object types with & to build a user card, mix shared fields into to-dos, see when a clash turns into never, and choose between & and extends.

By the end, you can
  • 01
    Combine types with &Write A & B for a value that has every property of both types, and tell it apart from A | B.
  • 02
    Mix in shared fieldsReuse small types like WithId across many object types, and read what happens when two parts name the same property.
  • 03
    Choose & or extendsExplain where each one reports a clash and what each one can combine, and pick the right one.

One object with both sets of properties

A site shows a small card for each user: their name and their email. Your code already has a type for each half. A Profile has a name, and a Contact has an email. The card needs both. You could write a third type that repeats every property, but TypeScript lets you combine the two you have:

A user card made of a profile and a contactPop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string };type Contact = { email: string };type UserCard = Profile & Contact; const asha: UserCard = { name: "Asha", email: "asha@example.com" };console.log(asha.name);console.log(asha.email);//               ^?

Lines 1 and 2 are two small type aliases: names for object types. Line 3 uses &, which you met briefly in Type aliases & interfaces. Profile & Contact is read "Profile and Contact", and it means a value that is both at the same time. This is called an intersection type, and the & sign makes it.

Line 5 makes asha, a UserCard, with a name and an email. Lines 6 and 7 print Asha and asha@example.com. The query on line 8 asks what TypeScript knows about email: (property) email: string. The email came from Contact, and TypeScript found it through the &. There are no errors.

Two cards, Profile with name: string and Contact with email: string, joined by an ampersand. Arrows from both lead to one card, UserCard, with name: string and email: string.Profilename: string&Contactemail: stringname: string; email: stringUserCard
Profile & Contact takes every property from both types. A UserCard must have a name and an email, like asha on line 5.
Real-life analogyA job that needs two skill lists

A food truck owner needs one person who can cook and can drive. The ad lists both skills. A great cook who can't drive doesn't get the job, and neither does a driver who can't cook. Every skill from both lists is required.

In real life: The cook's list: can cook
In TypeScript: Profile: has a name
In real life: The driver's list: can drive
In TypeScript: Contact: has an email
In real life: A food truck job asks for both lists
In TypeScript: Profile & Contact asks for both types
In real life: Only someone who can cook and drive gets the job
In TypeScript: Only an object with a name and an email fits
The main idea

A & B is the type of a value that is an A and a B at the same time. For object types, that means it has every property of both. It's a way to build a bigger type out of smaller ones.

& means and: every property from both

An intersection is most useful as a parameter type: the function can use every property from both types. Here a function shows a user card, and is called twice. The second call gets an error on purpose:

A card without an email (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string };type Contact = { email: string }; function showCard(user: Profile & Contact) {  return `${user.name} <${user.email}>`;} console.log(showCard({ name: "Ravi", email: "ravi@example.com" }));console.log(showCard({ name: "Asha" }));

Line 4 says showCard takes a user that is a Profile & Contact. Line 5 can read user.name and user.email, because the type promises both. Line 8 passes both, and prints Ravi <ravi@example.com>. Line 9 passes only a name, so TypeScript marks it:

  • Argument of type '{ name: string; }' is not assignable to parameter of type 'Profile & Contact' means "the value you passed doesn't fit what the function asks for". An argument is a value you pass to a function.
  • The next line of the message says why: Property 'email' is missing in type '{ name: string; }' but required in type 'Contact'. The object passes the Profile part and fails the Contact part.

Run it anyway, and line 9 prints Asha <undefined>: the card a user would see without the check.

Two objects go through two checks in a row, Profile and Contact. The object with a name and an email passes both and fits. The object with only a name passes Profile, fails Contact because it has no email, and doesn't fit.{ name, email }Profilehas a nameContacthas an emailfits{ name }Profilehas a nameContactno emaildoesn'tfit
Profile & Contact is two checks in a row. An object with a name and an email passes both. { name: "Asha" } has no email, so it fails the second check, like line 9.

You already know the other way to combine two types: the union Profile | Contact, read "Profile or Contact". The two signs look alike and mean opposite things. Try both: type a different type on line 3, and watch the list under the code as you type.

Playground: type the UserCard type
A user card made of two typesPop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string };type Contact = { email: string };type UserCard = Profile & Contact; const asha: UserCard = { name: "Asha", email: "asha@example.com" };console.log(asha.name, asha.email);
Line 3 saysProfile & Contact
What happens
TypeScriptNo errors
When it runsprints Asha asha@example.com
Try it yourself

Profile & Contact means the object has a name and an email. The object on line 5 has both, and line 6 can read both. No errors.

The code is checked by the real TypeScript compiler each time you type. Only line 3 changes. Reset puts Profile & Contact back.

With Profile & Contact there are no errors. With Profile | Contact, line 5 is still fine, because the object is a Profile. But line 6 gets two errors: a UserCard might be only a Contact, with no name, or only a Profile, with no email. With just Profile or just Contact, the object on line 5 has a property the type doesn't know. And each extra & { … } adds one more thing the object must have.

The table below puts the two signs side by side. Read each row as a question, then each sign's answer.

Profile | Contact and Profile & Contact, side by side
QuestionWith | (a union)With & (an intersection)
What must the value be?A Profile or a Contact. One of the two is enough.A Profile and a Contact, both at the same time.
Is { name: "Asha" } allowed?Yes. It's a Profile.No. It has no email, so it isn't a Contact.
What can you read without checking first?Only properties that both types have. Here, none.Every property of both: name and email.
Which one lets more objects in?This one. Each type you add is one more way to fit.Not this one. Each type you add is one more check to pass.

The name "intersection" comes from that last row. Think of all the objects that fit Profile, and all the objects that fit Contact. The intersection is the objects in both groups. That's fewer objects, and each one has more properties.

Mixing in shared fields

Data from a server often has the same few fields on every record: an id, and when it was created and changed. Instead of writing them into every type, you write them once, as small types, and add them where they're needed with &. This is called mixing in fields:

An id and timestamps mixed into two typesPop out in the code editor (opens in a new tab)TypeScript
type WithId = { id: number };type Timestamps = { createdAt: string; updatedAt: string }; type Todo = WithId & Timestamps & { title: string; done: boolean };type Note = WithId & Timestamps & { text: string }; function lastChange(item: WithId & Timestamps) {  return `#${item.id} changed on ${item.updatedAt}`;} const todo: Todo = { id: 1, title: "Buy milk", done: false, createdAt: "Oct 1", updatedAt: "Oct 2" };const note: Note = { id: 9, text: "Oat milk?", createdAt: "Oct 3", updatedAt: "Oct 3" };console.log(lastChange(todo));console.log(lastChange(note));

Line 1 is WithId: just an id. Line 2 is Timestamps: the two dates. Line 4 makes a Todo from three parts: an id, the timestamps, and its own title and done. Line 5 makes a Note from the same two shared parts and its own text.

The shared parts help functions too. Line 7's lastChange takes anything that is WithId & Timestamps, so lines 13 and 14 can pass it a to-do or a note. They print #1 changed on Oct 2 and #9 changed on Oct 3. A Todo has more properties than lastChange asks for, and that's fine: a variable with extra properties still fits.

Now build a to-do's type one part at a time. The object on line 5 stays the same; only the type on line 3 changes:

Playground: mix parts into a to-do
A to-do built from partsPop out in the code editor (opens in a new tab)TypeScript
type WithId = { id: number };type Timestamps = { createdAt: string };type Todo = { title: string; done: boolean }; const todo: Todo = { id: 1, title: "Buy milk", done: false };console.log(todo.title);
A Todo must havetitle, done
What happens
Line 5 hasid, title, done
TypeScript1 error: line 5
Try it yourself

The type lists only title and done, so the id on line 5 is unknown: Object literal may only specify known properties, and 'id' does not exist in type 'Todo'.

TypeScript checks the code for real. Each button changes only line 3; the object on line 5 stays the same. Reset goes back to Just the fields.

Each part you mix in adds properties the object must have. Just the fields is too few: the object's id is unknown. & WithId matches the object exactly. & WithId & Timestamps asks for a createdAt the object doesn't have.

JavaScript has its own ways to mix objects together while the code runs, and TypeScript types them with intersections. Object.assign(target, …sources) copies every property of the sources into the target object and gives the target back (the Mixins lesson uses it):

Object.assign gives back an intersectionPop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string };type Contact = { email: string }; const profile: Profile = { name: "Asha" };const contact: Contact = { email: "asha@example.com" }; const card = Object.assign({}, profile, contact);//    ^?console.log(card.name, card.email);

Lines 4 and 5 make a profile and a contact. Line 7 copies both into a new empty object. The query on line 8 shows TypeScript's answer: const card: Profile & Contact. TypeScript's own description of Object.assign says it returns the intersection of its arguments' types. Line 9 prints Asha asha@example.com.

When both types name the same property

So far, the two parts of each intersection had different properties. What if both have a property with the same name? Then the value must fit both property types at once. When one type is narrower, the narrower one wins:

A button whose variant must be dangerPop out in the code editor (opens in a new tab)TypeScript
type Button = { label: string; variant: string };type DangerButton = Button & { variant: "danger" }; const deleteButton: DangerButton = { label: "Delete", variant: "danger" };console.log(deleteButton.variant);//                       ^?

Line 1 says any Button has a variant that is a string. Line 2 adds a part where variant is the exact text "danger": a literal type, a type with one value. The only value that is a string and is "danger" is "danger". The query on line 6 confirms it: (property) variant: "danger". Line 5 prints danger.

Now two types that disagree. A site reads users from its database, where an id is a number. Its login service also describes users, but there an id is a string. Combining them goes wrong, on purpose here:

Two ids that can't both be true (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type DbUser = { id: number; name: string };type AuthUser = { id: string; email: string };type SessionUser = DbUser & AuthUser; const asha: SessionUser = { id: 7, name: "Asha", email: "asha@example.com" };console.log(asha.id);//               ^?

Line 3 combines the two, and TypeScript says nothing: the type itself is allowed. The trouble shows up on line 5, when you make a session user: Type 'number' is not assignable to type 'never'. The query on line 7 shows why: (property) id: never.

never is the type with no values at all. The id must be a number and a string at the same time, and no value is both. So any value you put there gets an error. When the code runs, the types are gone, and line 6 prints 7.

Two cards, DbUser with id: number and AuthUser with id: string, joined by an ampersand. Arrows lead to one card, id: never, with the note: no value is a number and a string.DbUserid: number&AuthUserid: stringid: neverno value is a number and a string
DbUser says id is a number, and AuthUser says it's a string. Nothing is both, so in DbUser & AuthUser the id is never: no value fits.

Sometimes the whole object type becomes never, not only one property. That happens when the clashing property holds literal types, like a status that is exactly "loading" or exactly "done":

Intersections that have no valuesPop out in the code editor (opens in a new tab)TypeScript
type Loading = { status: "loading" };type Done = { status: "done" }; type LoadingAndDone = Loading & Done;//   ^?type IdText = string & number;//   ^?

Lines 1 and 2 describe two states of a request. Line 4 asks for a request that is loading and done at once. The query on line 5 says type LoadingAndDone = never: no object can be in both states, so TypeScript drops the whole type. Line 6 does the same with two basic types. Nothing is a string and a number, so the query on line 7 says never too.

Intersections vs extends

Interfaces have their own way to build on other types: extends, from Type aliases & interfaces. An interface is another way to name an object type, and interface B extends A means "B has everything A has, plus what B adds". When nothing clashes, it gives the same result as &:

The same user card, written both waysPop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string };type Contact = { email: string }; type UserCard = Profile & Contact;interface UserCardShape extends Profile, Contact {} const asha: UserCard = { name: "Asha", email: "asha@example.com" };const same: UserCardShape = asha;console.log(same.email);

Line 4 builds UserCard with &. Line 5 builds UserCardShape with extends, from the same two parts; the empty {} means it adds nothing of its own. Line 8 stores a UserCard in a UserCardShape with no error, because the shapes match. Line 9 prints asha@example.com.

The difference shows up with a clash. Here are the database and login users again, combined with extends:

The same clash with extends (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type DbUser = { id: number; name: string };type AuthUser = { id: string; email: string }; interface SessionUser extends DbUser, AuthUser {}

This time TypeScript marks line 4, the type itself, on purpose here: Interface 'SessionUser' cannot simultaneously extend types 'DbUser' and 'AuthUser'. "Simultaneously" means "at the same time": the two parents disagree, and the next line of the message names the property, id. You find out where the mistake is, not later, when someone makes an object.

Left, with extends: an editor shows interface SessionUser extends DbUser, AuthUser, marked: the ids clash. Right, with an ampersand: an editor shows type SessionUser = DbUser & AuthUser with no mark, and below it an object with id: 7, marked: id can't hold 7.With extendseditorinterface SessionUserextends DbUser, AuthUsermarked: the ids clasherror on the typeWith &editorDbUser & AuthUserno mark here{ id: 7, … }marked: id can't hold 7error on the object
The same clash, found in two places. With extends, TypeScript marks the type itself. With &, the type is accepted, and the error only appears when you make an object, far from the cause.

& can do one thing extends can't: combine a union. Here a request is loading or done, and either way it has a url. Line 6 tries it with an interface, and gets an error on purpose:

Adding a url to a union (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Loading = { status: "loading" };type Done = { status: "done"; items: string[] };type Status = Loading | Done; type TodoRequest = Status & { url: string };interface TodoRequestShape extends Status { url: string } const request: TodoRequest = { status: "loading", url: "/todos" };console.log(request.status, request.url);

Line 3 names the union Status. Line 5 adds a url with &, which works: each choice gets a url. Line 6 says An interface can only extend an object type or intersection of object types with statically known members. "Statically known members" means properties TypeScript knows before the code runs. A union has two possible sets, so it isn't one. Line 9 prints loading /todos.

The table below compares the two. Read each row as a question, then each way's answer.

interface extends and type with &, side by side
Questioninterface … extendstype … = A & B
Do they make the same shape when nothing clashes?Yes. UserCardShape has a name and an email.Yes. UserCard has a name and an email, and the two types fit each other.
What if both parts have an id of different types?An error on the interface line, right where the mistake is.No error on the type. The id becomes never, and the error appears where you make an object.
Can it combine a union, like Loading | Done?No. An interface can only extend object types whose properties are known.Yes. Status & { url: string } adds a url to each choice.
Does it need a name of its own?Yes. An interface is always a named declaration.No. You can write Profile & Contact right in a parameter.
Which do TypeScript's performance notes suggest for big object types?This one. The compiler can reuse its checks of interfaces, and messages show their names.Use it where only & works, like with a union.

A simple rule: for object types you name and build on, use interface … extends, so clashes are reported where they are. Use & where extends can't go: unions, a quick combination in a parameter, or types that aren't objects.

Adding server fields to a saved draft

A blog editor saves a draft post. The server keeps the title and body, and adds its own fields: an id and the time it saved the draft. The save function's return type says exactly that:

A saved draft is the draft plus server fieldsPop out in the code editor (opens in a new tab)TypeScript
type Draft = { title: string; body: string };type Saved = { id: number; savedAt: string }; function saveDraft(draft: Draft): Draft & Saved {  return { ...draft, id: 42, savedAt: "10:30" };} const post = saveDraft({ title: "Hello", body: "My first post" });console.log(`${post.title} saved as #${post.id} at ${post.savedAt}`);

Line 1 is what the user writes. Line 2 is what the server adds. Line 4 says saveDraft takes a Draft and returns a Draft & Saved. Line 5 builds that with spread: ...draft copies the title and body (the Spread and rest lesson covers ...), and the two server fields are added after. Line 9 prints Hello saved as #42 at 10:30.

The return type also checks the function. Here line 5 forgets the time, on purpose:

A save that forgets savedAt (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Draft = { title: string; body: string };type Saved = { id: number; savedAt: string }; function saveDraft(draft: Draft): Draft & Saved {  return { ...draft, id: 42 };} const post = saveDraft({ title: "Hello", body: "My first post" });console.log(`${post.title} saved at ${post.savedAt}`);

TypeScript marks line 5: Type '{ id: number; title: string; body: string; }' is not assignable to type 'Draft & Saved', and the next line of the message says Property 'savedAt' is missing. Without the check, line 9 would print Hello saved at undefined.

Error or fine?
  • const card: { name: string } & { email: string } = { name: "Asha" };
  • const card: { name: string } & { email: string } = { name: "Asha", email: "a@b.c" };
  • const card: { name: string } | { email: string } = { name: "Asha" };
  • const id: string & number = "7";
  • const button: { variant: string } & { variant: "danger" } = { variant: "danger" };
  • const button: { variant: string } & { variant: "danger" } = { variant: "primary" };
  • const user = Object.assign({}, { name: "Asha" }, { email: "a@b.c" }); user.email;
  • interface Item { id: number } interface SavedItem extends Item { id: string }
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

& is only a type. It tells TypeScript what a value has, and it's removed before the code runs. To combine two real objects, you still use JavaScript:

Combining two objects while the code runsPop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string };type Contact = { email: string }; const profile: Profile = { name: "Asha" };const contact: Contact = { email: "asha@example.com" }; const both = { ...profile, ...contact };console.log(Object.keys(both));

Lines 4 and 5 make two objects. Line 7 copies both into a new one with spread. Line 8 prints its keys, name and email. Writing Profile & Contact somewhere would never do this copying for you.

  • "& keeps only the properties both types share." No. A Profile & Contact has every property of both. "Only what both share" is what you can read from a union.
  • "& joins two objects together." No. It's a type, removed before the code runs. Spread or Object.assign joins real objects.
  • "An intersection with a clash is an error." No. TypeScript accepts the type, and the clashing property becomes never. You see an error only when you make a value.
  • "The second type's property replaces the first." No. Both stay, and the value must fit both: string and "danger" leave only "danger".
  • "string & number is a value that is text or a number." No. That's string | number. Nothing is both, so string & number is never.
  • "& and extends are the same thing." No. They match when nothing clashes, but extends reports a clash on the type, and only & can combine a union.

Practice exercises

Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upFind what the card is missing

TypeScript will mark line 4. Which property does ravi need that it doesn't have?

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string; city: string };
type Contact = { email: string };

const ravi: Profile & Contact = { name: "Ravi", email: "ravi@example.com" };

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a property from an intersection

    What type does TypeScript give size on line 4? Guess first. Then point at size, or press Edit & run and add a // ^? line under it.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Settings = { theme: string } & { fontSize: number }; const settings: Settings = { theme: "dark", fontSize: 16 };const size = settings.fontSize;

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeMix an id into a product

      Change only line 3 so the errors go away. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type WithId = { id: number };type Product = { name: string; price: number };type SavedProduct = Product; const tea: SavedProduct = { id: 7, name: "Tea", price: 3 };console.log(`#${tea.id} ${tea.name} $${tea.price}`);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePredict a narrowed property

        Both parts of Shirt have a size. What type does size on line 4 have?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Shirt = { size: string } & { size: "S" | "M" | "L" }; const shirt: Shirt = { size: "M" };const size = shirt.size;

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix an id that can never hold a value

          TypeScript will mark line 5. What type does asha.id have in this code? Then change the types so a session user can keep both ids.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type DbUser = { id: number; name: string };
          type AuthUser = { id: string; email: string };
          type SessionUser = DbUser & AuthUser;
          
          const asha: SessionUser = { id: 7, name: "Asha", email: "asha@example.com" };
          console.log(asha.id);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeMove the error to where the mistake is

            TypeScript marks line 4, but the real mistake is on line 2. Rewrite line 2 with interface … extends. Which line does TypeScript mark now? Then fix the code.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type Button = { label: string; variant: string };type IconButton = Button & { variant: number; icon: string }; const save: IconButton = { label: "Save", variant: 1, icon: "disk" };

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask two things: what must a value have to fit the type, and what does the code do when it runs, after the types are gone? The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The intersection types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What must a value of type Profile & Contact have?

                Choose an answer to see the explanation.

              2. Question 2 of 7This card is typed with |. Which line does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Profile = { name: string };
                type Contact = { email: string };
                
                const asha: Profile | Contact = { name: "Asha", email: "asha@example.com" };
                console.log(asha.email);

                Choose an answer to see the explanation.

              3. Question 3 of 7What type does total have in Order & Receipt?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Order = { total: number };
                type Receipt = { total: string };
                
                const paid: Order & Receipt = { total: 7 };

                Choose an answer to see the explanation.

              4. Question 4 of 7TypeScript marks line 4. What does this code print when it runs anyway?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Order = { total: number };
                type Receipt = { total: string };
                
                const paid: Order & Receipt = { total: 7 };
                console.log(paid.total + 1);

                Choose an answer to see the explanation.

              5. Question 5 of 7With extends, where does TypeScript report the clash?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Order = { total: number };
                
                interface Receipt extends Order {
                  total: string;
                }

                Choose an answer to see the explanation.

              6. Question 6 of 7What type does TypeScript give shirt on line 6?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Size = { size: string };
                type Color = { color: string };
                
                const size: Size = { size: "M" };
                const color: Color = { color: "blue" };
                const shirt = Object.assign({}, size, color);

                Choose an answer to see the explanation.

              7. Question 7 of 7Which can add { url: string } to a union like Loading | Done?

                Choose an answer to see the explanation.

              Key takeaways

              • A & B is a value that is an A and a B at once. For objects, it has every property of both.
              • | means one or the other; & means both. A union lets more objects in; an intersection asks more of each one.
              • Small types like WithId and Timestamps can be mixed into many types, and Object.assign returns an intersection.
              • When both parts have the same property, the value must fit both types. A narrower type wins.
              • When the property types can't both be true, the property becomes never, and the error appears only when you make a value.
              • interface … extends reports a clash on the type itself; & can also combine unions and other types.

              Remember the one-liner.
              & asks for everything from both types, so the value must pass every check at once.

              Coming next: Declaration merging. Interfaces with the same name join together, which is how libraries let you add to their types, like the browser's Window.

              CompleteFrontend Clear concepts. Working examples.