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.
- 01Combine types with &Write
A & Bfor a value that has every property of both types, and tell it apart fromA | B. - 02Mix in shared fieldsReuse small types like
WithIdacross many object types, and read what happens when two parts name the same property. - 03Choose & 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:
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.
Profile & Contact takes every property from both types. A UserCard must have a name and an email, like asha on line 5.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 aname - In real life: The driver's list: can drive
- In TypeScript:
Contact: has anemail - In real life: A food truck job asks for both lists
- In TypeScript:
Profile & Contactasks for both types - In real life: Only someone who can cook and drive gets the job
- In TypeScript: Only an object with a
nameand anemailfits
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:
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 theProfilepart and fails theContactpart.
Run it anyway, and line 9 prints Asha <undefined>: the card a user would see without the check.
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.
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);Profile & ContactNo errorsprints Asha asha@example.comProfile & 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.
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.
| Question | With | (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:
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:
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);title, doneid, title, done1 error: line 5The 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'.
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):
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:
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:
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.
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":
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 &:
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:
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.
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:
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.
| Question | interface … extends | type … = 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:
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:
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.
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 }
Sort each line by what TypeScript says about it. Each line is checked on its own.
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:
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 & Contacthas 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.assignjoins 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:
stringand"danger"leave only"danger". - "
string & numberis a value that is text or a number." No. That'sstring | number. Nothing is both, sostring & numberisnever. - "
&andextendsare the same thing." No. They match when nothing clashes, butextendsreports 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.
TypeScript will mark line 4. Which property does ravi need that it doesn't have?
type Profile = { name: string; city: string };
type Contact = { email: string };
const ravi: Profile & Contact = { name: "Ravi", email: "ravi@example.com" };type Profile = { name: string; city: string };type Contact = { email: string }; const ravi: Profile & Contact = { name: "Ravi", city: "Pune", email: "ravi@example.com" };Profile & Contact needs name and city from Profile, and email from Contact. ravi has no city, so TypeScript marks line 4 with Property 'city' is missing. Adding city: "Pune" fixes it.
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.
type Settings = { theme: string } & { fontSize: number }; const settings: Settings = { theme: "dark", fontSize: 16 };const size = settings.fontSize;type Settings = { theme: string } & { fontSize: number }; const settings: Settings = { theme: "dark", fontSize: 16 };const size = settings.fontSize;// ^?Settings has every property of both parts, and the second part says fontSize is a number. So size is a number, as the query on line 5 shows.
Change only line 3 so the errors go away. What does the fixed code print?
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}`);type WithId = { id: number };type Product = { name: string; price: number };type SavedProduct = Product & WithId; const tea: SavedProduct = { id: 7, name: "Tea", price: 3 };console.log(`#${tea.id} ${tea.name} $${tea.price}`);Product & WithId adds the id from line 1 to the product's own fields. Line 5 now fits, line 6 can read tea.id, and it prints #7 Tea $3.
Both parts of Shirt have a size. What type does size on line 4 have?
type Shirt = { size: string } & { size: "S" | "M" | "L" }; const shirt: Shirt = { size: "M" };const size = shirt.size;type Shirt = { size: string } & { size: "S" | "M" | "L" }; const shirt: Shirt = { size: "M" };const size = shirt.size;// ^?Every one of "S", "M", and "L" is also a string, so the narrower part wins. The query on line 5 shows const size: "S" | "M" | "L".
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.
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);type DbUser = { id: number; name: string };type AuthUser = { authId: string; email: string };type SessionUser = DbUser & AuthUser; const asha: SessionUser = { id: 7, authId: "u-7", name: "Asha", email: "asha@example.com" };console.log(asha.id, asha.authId);In the starter, id must be a number and a string, so its type is never and the 7 on line 5 gets an error. Renaming the login service's field to authId removes the clash. The session user keeps both ids, and it prints 7 u-7.
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.
type Button = { label: string; variant: string };type IconButton = Button & { variant: number; icon: string }; const save: IconButton = { label: "Save", variant: 1, icon: "disk" };type Button = { label: string; variant: string };interface IconButton extends Button { icon: string } const save: IconButton = { label: "Save", variant: "icon", icon: "disk" };console.log(save.variant);With extends, the clash is reported on line 2: Interface 'IconButton' incorrectly extends interface 'Button', because a number can't replace a string. Leaving variant to Button and using a string value fixes it, and it prints icon.
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.
Question 1 of 7What must a value of type
Profile & Contacthave?Choose an answer to see the explanation.
Question 2 of 7This card is typed with
|. Which line does TypeScript flag?Read the code, then predicttype 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.
Question 3 of 7What type does
totalhave inOrder & Receipt?Read the code, then predicttype Order = { total: number }; type Receipt = { total: string }; const paid: Order & Receipt = { total: 7 };Choose an answer to see the explanation.
Question 4 of 7TypeScript marks line 4. What does this code print when it runs anyway?
Read the code, then predicttype 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.
Question 5 of 7With
extends, where does TypeScript report the clash?Read the code, then predicttype Order = { total: number }; interface Receipt extends Order { total: string; }Choose an answer to see the explanation.
Question 6 of 7What type does TypeScript give
shirton line 6?Read the code, then predicttype 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.
Question 7 of 7Which can add
{ url: string }to a union likeLoading | Done?Choose an answer to see the explanation.
Key takeaways
A & Bis a value that is anAand aBat 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
WithIdandTimestampscan be mixed into many types, andObject.assignreturns 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 … extendsreports 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.