Type aliases & interfaces
Give a type a name with type or interface, build one type on another with extends and &, and choose between them, with code you can edit and break.
- 01Name a typeWrite a type alias or an interface once and use its name everywhere, so one change is checked at every use.
- 02Build on a typeMake one type from another with
interface … extendsor with&, and read the error when a property clashes. - 03Choose between themSay what only a type alias can do, what only an interface can do, and pick one for an object, a union, or a library type.
Write a type once, use its name everywhere
A shop page has two small functions. One builds a label for a product, like Tea: $3. The other adds up a cart. Both take products, so both write out what a product is:
function productLabel(product: { name: string; price: number }) { return `${product.name}: $${product.price}`;} function cartTotal(items: { name: string; price: number }[]) { let total = 0; for (const item of items) total += item.price; return total;} console.log(productLabel({ name: "Tea", price: 3 }));console.log(cartTotal([{ name: "Tea", price: 3 }, { name: "Cake", price: 4 }]));Line 1 says productLabel takes a product with a name that is a string and a price that is a number. Line 2 builds the label with a template literal. Line 5 writes the same shape again, with [] after it: cartTotal takes a list of those products. Lines 6 to 8 add up the prices. Line 11 prints Tea: $3 and line 12 prints 7. There are no errors.
The code works, but { name: string; price: number } appears twice. Each copy is long to read. And when products get a new property, you have to find every copy and change each one. Miss one, and two parts of the page disagree about what a product is.
TypeScript lets you give a type a name. You write the type once, and then use the name wherever the type goes. A name made with the type keyword is called a type alias. ("Alias" means "another name for the same thing".) Here is the same code with one:
type Product = { name: string; price: number }; function productLabel(product: Product) { return `${product.name}: $${product.price}`;} function cartTotal(items: Product[]) { let total = 0; for (const item of items) total += item.price; return total;} const tea: Product = { name: "Tea", price: 3 };// ^?const cake: Product = { name: "Cake", price: "4" };console.log(productLabel(tea));console.log(cartTotal([tea, cake]));Line 1 is new: type Product = … gives the name Product to the object type. Read it as "a Product is an object with a string name and a number price". Line 3 now says product: Product, and line 7 says items: Product[], a list of products. Line 13 makes the tea, and the query on line 14 shows its type by name: const tea: Product.
Line 15 gives the cake a price in quotes, and TypeScript marks it on purpose, because it's a bug: Type 'string' is not assignable to type 'number'. That means "you're putting text where a number has to go". The name changed nothing about the checking: TypeScript looked up what Product means and checked the cake against it. Run it anyway, and line 16 prints Tea: $3, but line 17 prints 34, because 3 + "4" joins the two as text.
Product. The parameters product: Product and items: Product[] both use the name, so both mean that one shape.The name pays off when the type changes. The playground below has two products and the label function. Each button changes only line 1, the type. Watch which lines get errors:
type Product = { name: string; price: number }; const tea: Product = { name: "Tea", price: 3 };const cake: Product = { name: "Cake", price: 4 }; function productLabel(product: Product) { return `${product.name}: $${product.price}`;}console.log(productLabel(tea));Tea: $33, 4, 6No errorsLine 1 says a product has a name and a price. Lines 3 and 4 match it, and line 7 reads a price that exists. No errors, and the code prints Tea: $3.
Add inStock changes one line, and TypeScript marks lines 3 and 4: both products are now missing inStock. Add inStock? makes it optional, with a question mark, so the errors go. Remove price marks three lines: the two objects that still have a price, and line 7, which reads one. You changed the type in one place, and TypeScript gave you a list of every place to update. The output stays Tea: $3 each time, because types never change what the code does.
Typing your full address for every order is slow, and one typo sends a parcel to the wrong street. So a delivery app lets you save the address once, under a short name like Home. At checkout you just pick Home. When you move, you edit Home in one place, and every new order uses the new address.
- In real life: The full address: 12 Lake Road, Pune
- In TypeScript: The type:
{ name: string; price: number } - In real life: You save it once under the name Home
- In TypeScript: You write
type Product = …once - In real life: At checkout, you pick Home
- In TypeScript: You write
product: Product - In real life: You edit Home once, and every new order uses the new address
- In TypeScript: You change the type once, and TypeScript checks every place that uses
Product
A type alias or an interface gives a type a name. You write the type once, like type Product = { name: string; price: number }, and use the name everywhere the type goes. TypeScript checks each use against what the name means. A type alias can name any type; an interface names an object shape.
Type aliases: a name for any type
A type alias is the word type, a name, an equals sign, and a type. Names of types start with a capital letter by habit, like Product, so you can tell them apart from variables. The type after the = can be any type at all, not only an object:
type Plan = "free" | "pro";type Email = string;type User = { name: string; email: Email; plan: Plan }; const asha: User = { name: "Asha", email: "asha@example.com", plan: "pro" };const ben: User = { name: "Ben", email: "ben@example.com", plan: "gold" };console.log(asha.plan);Line 1 names a union: a Plan is the exact text "free" or the exact text "pro", and nothing else (a type that is one exact value is a literal type). Line 2 gives string a second name, Email, which tells a reader what the text is for. Line 3 names an object type, and its properties use the two aliases from lines 1 and 2. Line 5 makes Asha, on the pro plan, which fits.
Line 6 puts Ben on a "gold" plan, and TypeScript marks it on purpose: Type '"gold"' is not assignable to type 'Plan'. The message uses the name, so you can read it as ""gold" isn't one of the plans". Line 7 still prints pro when it runs.
Try it yourself. The box below fills in the type on line 1 of a ProductId alias. Line 3 gives the tea the id 7, and line 4 gives the cake the id "c-4". Type different types and watch the list under the code:
type ProductId = number; const teaId: ProductId = 7;const cakeId: ProductId = "c-4";console.log(teaId, cakeId);number1 error: line 4prints 7 c-4ProductId means number. Line 3's 7 fits, but line 4's "c-4" is text: Type 'string' is not assignable to type 'number'.
With number, the cake's text id gets an error; with string, the tea's number id does. With number | string, both fit and the errors are gone. Whatever you type, the code prints 7 c-4 when it runs: the alias only tells TypeScript what to check.
An alias doesn't make a new type. It's another name for the same type, and TypeScript treats the two as one:
type Email = string; const email: Email = "asha@example.com";// ^?const loud = email.toUpperCase();console.log(loud);Line 1 makes Email another name for string. Line 3 says email is an Email. The query on line 4 shows const email: string: for an alias of a simple type like string, TypeScript shows the type itself. Line 5 calls a string method, and line 6 prints ASHA@EXAMPLE.COM. Any string fits an Email, even "hello", because the two names mean the same type. (The Structural typing lesson shows more of this.)
Interfaces: a name for an object shape
The second way to name a type is an interface. You write the word interface, a name, and the properties inside braces. There's no equals sign. An interface can only describe an object shape: which properties an object has, and the type of each.
interface User { name: string; email: string; nickname?: string;} function greet(user: User) { return `Hi, ${user.nickname ?? user.name}`;} const asha: User = { name: "Asha", email: "asha@example.com" };// ^?console.log(greet(asha));console.log(greet({ name: "Ben" }));Lines 1 to 5 are the interface. A User has a name and an email, both strings, and may have a nickname: the question mark on line 4 makes it optional, just as in an object type. Line 7 says greet takes a User. Line 8 uses the nickname if there is one, or the name otherwise, with ??. Line 11 makes Asha, and the query on line 12 shows const asha: User. Line 13 prints Hi, Asha.
Line 14 passes Ben with no email, and TypeScript marks it on purpose. The message has two lines:
Argument of type '{ name: string; }' is not assignable to parameter of type 'User'means "the object you passed doesn't fit whatgreetasks for".Property 'email' is missing in type '{ name: string; }' but required in type 'User'says why: aUsermust have an email, and Ben's object has none.
Run it anyway and line 14 prints Hi, Ben: the function only reads names, so the missing email doesn't show yet. It would, the first time some other code sent Ben an email.
For an object, a type alias and an interface mean the same thing. TypeScript compares the shapes, so one can stand in for the other:
type Product = { name: string; price: number }; interface ShopItem { name: string; price: number;} const tea: Product = { name: "Tea", price: 3 };const sameTea: ShopItem = tea;console.log(sameTea.price);Line 1 names the shape with a type alias, Product. Lines 3 to 6 name the same shape with an interface, ShopItem. Line 8 makes a Product, and line 9 stores it as a ShopItem with no error, because both names describe an object with a string name and a number price. Line 10 prints 3.
Building one type on another
Often one type is another type plus a little more. An admin on a shop's account page is a user with a list of things they are allowed to do. You don't want to copy the user's properties into the admin; you want to say "an admin is a user, plus this". An interface says it with extends:
interface User { name: string; email: string;} interface Admin extends User { permissions: string[];} const lee: Admin = { name: "Lee", email: "lee@example.com", permissions: ["refunds"] };console.log(`${lee.name} can do: ${lee.permissions.join(", ")}`);Lines 1 to 4 make User, with a name and an email. Line 6 makes Admin and says it extends User: an Admin has everything a User has. Line 7 adds what only admins have, a list of permissions (string[] is a list of strings). Line 10 makes Lee, who needs all three properties. Line 11 prints Lee can do: refunds.
Admin starts with everything User has, then adds permissions. An interface writes this with extends; a type alias writes it with &.A type alias can't use extends. It builds on another type with &, which means "everything in this type and everything in that one". A type made with & is called an intersection:
type User = { name: string; email: string;}; type Admin = User & { permissions: string[];}; const lee: Admin = { name: "Lee", email: "lee@example.com", permissions: ["refunds"] };// ^?console.log(`${lee.name} can do: ${lee.permissions.join(", ")}`);Lines 1 to 4 are User as a type alias. Line 6 says an Admin is a User & the object type on lines 6 to 8, so it has a name, an email, and permissions. Line 10 makes the same Lee, the query on line 11 shows const lee: Admin, and line 12 prints the same Lee can do: refunds.
So far the two ways give the same result. They differ when the new type gets a property wrong. Below, User says a plan is "free" or "pro", and line 6 builds Admin on it. Press each button to change line 6:
interface User { name: string; plan: "free" | "pro";} interface Admin extends User { permissions: string[] } const lee: Admin = { name: "Lee", plan: "pro", permissions: ["refunds"] };console.log(lee.name + " has " + lee.plan);Lee has prointerface … extendsNo errorsinterface Admin extends User means an admin has everything a user has, plus permissions. Line 8 fits, and the code prints Lee has pro.
extends and & have no errors. extends, plan: number gives plan a different type from the one User has, and TypeScript marks line 6 itself: Interface 'Admin' incorrectly extends interface 'User', which means "an Admin would no longer be a User". &, plan: number gets no error on line 6. No value can be a number and "pro" at once, so Admin becomes never, a type that no value fits, and the errors appear later, on lines 8 and 9, where the type is used. With extends, the error points at the real mistake. (The Intersection types lesson covers & in depth.)
An interface can also extend more than one type, and it can extend a type alias that names an object:
type Contact = { email: string }; interface Address { city: string;} interface Customer extends Contact, Address { name: string;} const asha: Customer = { name: "Asha", email: "asha@example.com", city: "Pune" };console.log(`${asha.name} lives in ${asha.city}`);Line 1 is a type alias, Contact, with an email. Lines 3 to 5 are an interface, Address, with a city. Line 7 says a Customer extends both, with a comma between them, and line 8 adds a name. Line 11 makes Asha with all three properties, and line 12 prints Asha lives in Pune. The two ways mix freely.
Which one to use
For most object shapes, you can use either one. A few things only one of them can do. You've seen the first: only a type alias can name a union, an exact value, or a simple type like string. The second is about declaring the same name twice. Two interfaces with the same name join into one:
interface Settings { theme: string;} interface Settings { fontSize: number;} const settings: Settings = { theme: "dark", fontSize: 16 };console.log(settings.fontSize);Lines 1 to 3 make a Settings interface with a theme. Lines 5 to 7 make another one with the same name and a fontSize. TypeScript joins them, so a Settings needs both. This is called declaration merging. Line 9 makes settings with both properties, and line 10 prints 16. Libraries use merging to let you add properties to their types; a later lesson, Declaration merging, shows how.
type Settings = { theme: string };type Settings = { fontSize: number };A type alias can't be declared twice. TypeScript marks both lines on purpose: Duplicate identifier 'Settings', which means "this name is already taken". An identifier is a name in your code.
interface twice and TypeScript joins the two into one Settings with both properties. Declare a type twice and you get Duplicate identifier 'Settings'.The table below sums up the differences. Read each row as a question, then the answer for each keyword.
| Question | type | interface |
|---|---|---|
| Can it name an object shape? | Yes: type User = { name: string }. | Yes: interface User { name: string }. |
| Can it name a union, an exact value, or a primitive? | Yes: type Plan = "free" | "pro". | No. An interface always describes an object shape. |
| How do you build on another type? | With &: type Admin = User & { … }. | With extends: interface Admin extends User { … }. |
| Where is a clashing property reported? | Where you use the type, because the property, or the whole type, becomes never. | On the interface line itself. |
| What if you declare the same name twice? | An error: Duplicate identifier. | The two declarations merge into one. |
| Is it there when the code runs? | No. TypeScript removes it. | No. TypeScript removes it. |
The TypeScript handbook says you can mostly choose by taste, and suggests using interface until you need something only type can do. A simple rule that follows from the table:
- Use
interfacefor object shapes, especially ones you build on withextends, because a clash is reported where it happens. - Use
typefor everything an interface can't name: unions like"free" | "pro", and other names likeEmail. - Whatever you choose for objects, choose it the same way across a project, so readers aren't left wondering why.
Users and admins on an account page
Here is how the two work together on a real page. An account page shows a badge with each person's name, and adds (Pro) for pro users. Some people are admins:
type Plan = "free" | "pro"; interface User { id: number; name: string; plan: Plan;} interface Admin extends User { permissions: string[];} function badge(user: User) { return user.plan === "pro" ? `${user.name} (Pro)` : user.name;} const asha: User = { id: 1, name: "Asha", plan: "pro" };const lee: Admin = { id: 2, name: "Lee", plan: "free", permissions: ["refunds"] };console.log(badge(asha));console.log(badge(lee));Line 1 uses a type alias for the plans, because it's a union. Lines 3 to 7 use an interface for a user, with an id, a name, and a plan. Lines 9 to 11 build Admin on User with extends. Lines 13 to 15 make the badge: line 14 adds (Pro) when the plan is "pro".
Line 17 makes Asha, a user, and line 18 makes Lee, an admin. Line 19 prints Asha (Pro). Line 20 passes Lee to badge, which asks for a User, and there's no error: an admin has everything a user has, so an admin fits wherever a user goes. It prints Lee.
type Plan = "free" | "pro";interface Plan = "free" | "pro";interface User { name: string } const asha: User = { name: "Asha" };type User = { name: string }; type User = { email: string };interface User { name: string } interface User { email: string }type Email = string; const email: Email = 7;interface User { name: string } interface Admin extends User { name: number }type User = { name: string }; interface Admin extends User { level: number }
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
Type aliases and interfaces are only for TypeScript. Neither one becomes code that runs. Here is a small file with both:
type Plan = "free" | "pro"; interface User { name: string; plan: Plan;} const asha: User = { name: "Asha", plan: "pro" };console.log(asha.plan);Line 1 is a type alias and lines 3 to 6 are an interface. Line 8 makes a real object, and line 9 prints pro. When TypeScript turns this into JavaScript, it removes the alias, the interface, and the : User annotation:
const asha = { name: "Asha", plan: "pro" };
console.log(asha.plan);Only the object and the log are left, and the code still prints pro. You can't use User as a value, for example with typeof or instanceof, because it doesn't exist when the code runs.
profile.ts with a type alias and an interface. TypeScript removes both, so profile.js has only the object and the log. Nothing called Plan or User exists while the page runs.- "A type alias makes a new type." No. It's another name for the same type. Any string fits
type Email = string, and hovering showsstring. - "An interface checks objects more strictly than a type alias." No. For the same shape, both check the same way, and one can stand in for the other.
- "Interfaces exist when the code runs, like classes." No. Both are removed. Nothing called
Useris left inprofile.js. - "A type alias can't be extended." No.
&builds on it, and an interface can useextendson a type alias that names an object, asCustomerdid withContact. - "An interface can name any type." No. It always describes an object shape. For a union like
"free" | "pro", use a type alias. - "A project must use only one of them." No. They mix freely: the account page uses a type alias for the plans and interfaces for the people.
Practice exercises
Each exercise is TypeScript. When you write or 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.
Two products, one with a mistake. Which line will TypeScript mark?
interface Product {
name: string;
price: number;
}
const tea: Product = { name: "Tea", price: 3 };
const cake: Product = { name: "Cake" };interface Product { name: string; price: number;} const tea: Product = { name: "Tea", price: 3 };const cake: Product = { name: "Cake" }; // line 7: price is missingThe cake on line 7 has no price, and the interface says every Product has one. TypeScript reports Property 'price' is missing in type '{ name: string; }' but required in type 'Product' there.
The order's shape is written twice. Give it a name with a type alias and use the name in both places. What does your code print?
function orderLabel(order: { id: number; city: string }) { return `Order ${order.id} to ${order.city}`;} const first: { id: number; city: string } = { id: 7, city: "Pune" };console.log(orderLabel(first));type Order = { id: number; city: string }; function orderLabel(order: Order) { return `Order ${order.id} to ${order.city}`;} const first: Order = { id: 7, city: "Pune" };console.log(orderLabel(first));Line 1 names the shape once. The parameter and the variable both say Order, so a change to the shape happens in one place. The code prints Order 7 to Pune, the same as before: names don't change what runs.
greet uses a User type that doesn't exist yet. Write it as an interface. What does the fixed code print?
function greet(user: User) { return `Hi, ${user.name}! You have ${user.cartItems} items.`;} console.log(greet({ name: "Asha", cartItems: 2 }));interface User { name: string; cartItems: number;} function greet(user: User) { return `Hi, ${user.name}! You have ${user.cartItems} items.`;} console.log(greet({ name: "Asha", cartItems: 2 }));greet reads a name that is text and a cartItems that is a number, so the interface lists those two. With User defined, the error is gone, and the code prints Hi, Asha! You have 2 items.
TypeScript marks line 10. Fix it. Which property did you add?
interface User { name: string; email: string;} interface Admin extends User { permissions: string[];} const lee: Admin = { name: "Lee", permissions: ["refunds"] };console.log(lee.email);interface User { name: string; email: string;} interface Admin extends User { permissions: string[];} const lee: Admin = { name: "Lee", email: "lee@example.com", permissions: ["refunds"] };console.log(lee.email);An Admin has everything a User has, so Lee needs an email as well as permissions. With the email added, the error is gone, and line 11 prints lee@example.com instead of undefined.
Line 5 looks like it matches Settings, but TypeScript marks it. Which property does it say is missing, and where does that property come from?
interface Settings {
theme: string;
}
const settings: Settings = { theme: "dark" };
interface Settings {
fontSize: number;
}interface Settings { theme: string;} const settings: Settings = { theme: "dark", fontSize: 16 }; interface Settings { fontSize: number;} console.log(settings.fontSize);The two Settings interfaces merge, so a Settings needs a theme and a fontSize, even though the second declaration comes after line 5. TypeScript reports Property 'fontSize' is missing. Adding fontSize: 16 fixes it, and the log prints 16.
The shop adds a team plan, and TeamUser on line 8 gets an error. Fix it by changing one line. Which value do you add to Plan?
type Plan = "free" | "pro"; interface User { name: string; plan: Plan;} interface TeamUser extends User { plan: "team"; seats: number;}type Plan = "free" | "pro" | "team"; interface User { name: string; plan: Plan;} interface TeamUser extends User { plan: "team"; seats: number;} const crew: TeamUser = { name: "Crew", plan: "team", seats: 5 };console.log(`${crew.name}: ${crew.seats} seats`);extends checks that every property TeamUser shares with User still fits. "team" wasn't a Plan, so line 8 got Interface 'TeamUser' incorrectly extends interface 'User'. Adding "team" to Plan fixes it, and the new team user prints Crew: 5 seats.
Check your understanding
For each question, ask what each name means: which properties or values it allows, and whether it's still there when the code runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
type Product = { name: string; price: number }make?Choose an answer to see the explanation.
Question 2 of 7Which of these can an interface name?
Choose an answer to see the explanation.
Question 3 of 7Two interfaces share a name. What does TypeScript say about line 4?
Read the code, then predictinterface Settings { theme: string } interface Settings { fontSize: number } const settings: Settings = { theme: "dark" };Choose an answer to see the explanation.
Question 4 of 7Which line does TypeScript mark in this extends?
Read the code, then predictinterface User { name: string; plan: "free" | "pro" } interface Admin extends User { plan: number } const lee: Admin = { name: "Lee", plan: 1 };Choose an answer to see the explanation.
Question 5 of 7What does hovering
emailon line 2 show?Read the code, then predicttype Email = string; const email: Email = "asha@example.com";Choose an answer to see the explanation.
Question 6 of 7This code has a type error. What does it print when it runs anyway?
Read the code, then predictinterface User { name: string } const kind = typeof User; console.log(kind);Choose an answer to see the explanation.
Question 7 of 7Can an Admin go where a User is expected, and what prints?
Read the code, then predictinterface User { name: string } interface Admin extends User { permissions: string[] } function greet(user: User) { return `Hi, ${user.name}`; } const lee: Admin = { name: "Lee", permissions: [] }; console.log(greet(lee));Choose an answer to see the explanation.
Key takeaways
- A type alias,
type Product = …, gives any type a name: an object, a union, or a simple type likestring. - An interface,
interface User { … }, gives an object shape a name. - Write a type once and use its name everywhere. One change is then checked at every use.
- Build on a type with
interface Admin extends Userortype Admin = User & { … }.extendsreports a clash on the interface itself. - Interfaces with the same name merge; type aliases with the same name are an error.
- Neither is there when the code runs, and an alias is never a new type, just a second name.
Remember the one-liner.
Name a type once and use the name everywhere: type for any type, interface for object shapes you build on.
Coming next: null, undefined & strictNullChecks. It shows how TypeScript makes you handle a value that might be missing, like a user with no nickname.