Type Aliases and Interfaces in TypeScript: Giving Your Types a Name
We give types names with type aliases and interfaces, compare extends with intersections, look at declaration merging, and cover when to pick which one and how to name them well.
Version note: The code in this post targets TypeScript 7.0 and Node 26.
Predict First
In the previous post we wrote object types inline. In this one we'll give them names. Before we start, a quick question: does the code below compile? If it errors, on which line?
interface User {
name: string;
}
interface User {
age: number;
}
const ada: User = { name: "Ada" };
We wrote two separate interfaces with the same name. Your first guess might be "a name clash error on the second declaration." Don't rush; we'll answer when we get to declaration merging.
Writing the Same Shape Everywhere
The inline object types we saw earlier do the job in small examples. But once the same shape starts showing up in more than one place, trouble begins:
function printUser(user: { name: string; email: string; age?: number }) {
console.log(`${user.name} <${user.email}>`);
}
function isAdult(user: { name: string; email: string; age?: number }) {
return (user.age ?? 0) >= 18;
}
These two functions expect the same user shape, but you're the only one who knows that. TypeScript just sees two similar-looking types in two separate places. Tomorrow, when you need to add a phone property to the user, you have to find and update every spot by hand; miss one and the inconsistency quietly settles into the code. The fix is to give this shape a name and use that name everywhere. TypeScript offers two tools for this: type and interface.
Type Aliases: Giving a Type a Name
type User = {
name: string;
email: string;
age?: number;
};
function printUser(user: User) {
console.log(`${user.name} <${user.email}>`);
}
function isAdult(user: User) {
return (user.age ?? 0) >= 18;
}
The type keyword gives a name to the type on the right of the equals sign. This is called a type alias. Optional and readonly properties, index signatures, everything we saw in the previous post works exactly the same. The only change is that you write the type once and use its name everywhere. When the shape needs to change, you change one place, and TypeScript rechecks every spot that uses that type.
The real strength of a type alias is that it isn't limited to objects. You can put any type on the right of the equals sign:
type Point = [number, number];
type Tags = readonly string[];
type UserId = string;
const center: Point = [0, 0];
const tags: Tags = ["typescript", "basics"];
const id: UserId = "u-42";
There's an important subtlety here: an alias doesn't create a new type, it gives an existing type a second name. To TypeScript, UserId and string are exactly the same thing; you can pass any string where a UserId is expected. The name carries meaning for the human reading the code, not for the compiler. We'll see identity types that really are distinguishable from each other in the advanced series.
Interfaces: Describing the Shape of an Object
interface Product {
readonly id: string;
title: string;
price: number;
discount?: number;
describe(): string;
}
const keyboard: Product = {
id: "p-1",
title: "Mechanical Keyboard",
price: 89,
describe() {
return `${this.title}: ${this.price}`;
},
};
An interface exists only for object shapes. There's no equals sign; the curly braces come right after the name. Inside, you write the properties and methods. The describe(): string line says there's a method that takes no parameters and returns a string. Separating members with semicolons is the convention, but commas are accepted too.
In this form there's practically no difference between type Product = { ... } and interface Product { ... }. You use both in the same places, in the same way, and they behave the same in the editor and in error messages. The differences show up in two areas: when you extend a type, and when you use the same name a second time.
Extending: extends and Intersections
A common need: take an existing shape and add a few properties on top. With an interface you do this with extends:
interface Entity {
id: string;
createdAt: Date;
}
interface Customer extends Entity {
name: string;
email: string;
}
const customer: Customer = {
id: "c-1",
createdAt: new Date(),
name: "Grace",
email: "grace@example.com",
};
Customer takes all the properties of Entity and adds its own. You can also extend more than one interface by separating them with commas: interface Admin extends Customer, Auditable. An interface can even extends a type alias that describes an object shape, so mixing the two in one project is no problem.
Type aliases have no extends keyword. You get the same result with an intersection, using the & operator:
type Entity = {
id: string;
createdAt: Date;
};
type Customer = Entity & {
name: string;
email: string;
};
A & B means "has all the properties of both A and B." Most of the time the two approaches give the same result. They part ways when the two sides declare incompatible types for the same property:
interface Base {
id: string;
}
// Error: Interface 'Item' incorrectly extends interface 'Base'.
interface Item extends Base {
id: number;
}
type BaseShape = { id: string };
type ItemShape = BaseShape & { id: number };
// Error: Type 'number' is not assignable to type 'never'.
const item: ItemShape = { id: 1 };
extends catches the conflict right at the declaration, with a clear message: Item's id is a number, but Base wants a string. The intersection doesn't object at the declaration at all. id has to be a string and a number at the same time; since no such value can exist, its type becomes never. You only see the error when you try to use the type, and with a message that tells you far less. We'll cover intersections in detail, and the cases where they really shine, in a separate post.
Declaration Merging: Two Interfaces with the Same Name
Back to the opening question. The second interface User declaration doesn't error. The error is on the last line:
error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'User'.
When you write more than one interface with the same name in the same scope, TypeScript merges them into a single interface. This is called declaration merging. Once the two declarations merge, User requires both name and age, and we only provided name.
Type aliases don't merge like this. Writing a type with the same name a second time is simply an error, and TypeScript flags both declarations:
// Error: Duplicate identifier 'Account'.
type Account = { id: string };
// Error: Duplicate identifier 'Account'.
type Account = { email: string };
So what is this interface behavior good for? Its main use is extending types someone else wrote. For example, when you want to add your own property to the browser's global Window type, or fill in a field missing from a library's types, you write an interface with the same name and TypeScript merges the two. We'll look at this technique in detail in the post on declaration files. In everyday code, though, you rarely use declaration merging on purpose; it's more likely to happen to you without you noticing. We'll see how in the pitfalls section.
Which One Should You Use When?
This question has been debated in the TypeScript community for years, and both sides have reasonable arguments. Here's a practical way to split it.
Where a type alias is required: unions (type Id = string | number), tuples, names for primitive types, function types, and the mapped and conditional types we'll see in later series. An interface can't express any of these; it only describes object shapes.
Where interfaces stand out: object shapes you expose to others and that others might extend, and contracts that classes conform to with implements. We'll cover classes in detail in the intermediate series. extends catches conflicts early, and the interface's name shows up in error messages. The TypeScript team's performance guidance also recommends interfaces built with extends over types built with intersections in large projects, because the compiler can cache relationships between interfaces more easily.
My suggestion is simple: interfaces for object shapes, types for everything else. But the real rule is consistency. Whichever one your team picks, use it everywhere in the project. A "this file uses interfaces, that one uses types" mix wastes far more time than the small differences between them.
Naming
Type and interface names are written in PascalCase: User, OrderItem, PaymentStatus. Since variables and functions are camelCase, when you see a name starting with a capital letter you know right away that it's a type or a class.
The I prefix (IUser) that people coming from C# or Java are used to isn't common in TypeScript; the TypeScript team's own coding guidelines say not to use it. Likewise, suffixes like UserType or UserInterface add no information; the code itself already tells you it's a type. Pick the name based on what the type represents: User is a user, UserSummary is the shortened version you show in a list, NewUserInput is the raw data coming from a form. A well-chosen name tells you what a type is for without looking inside it.
Common Pitfalls
The first pitfall is declaration merging you didn't ask for. A file with no import or export at all is a script to TypeScript, and its declarations go into the global scope. And the global scope already holds the browser's DOM types. Look at this code:
interface Comment {
text: string;
}
// Error: Type '{ text: string; }' is missing the following properties from type 'Comment': data, length, ownerDocument, appendData, and 59 more.
const comment: Comment = { text: "Nice post!" };
You think you're describing a blog comment, but the DOM already has a Comment interface representing comment nodes in HTML. Yours merged with it, and now it expects more than sixty properties. The same can happen with names like Response, Location or Text. The fix is either to make the file a module (one import or export is enough, even export {};) or to pick a more specific name: BlogComment. In real projects almost every file is already a module, so you mostly fall into this trap in single-file experiments. Had you written the same name as a type alias, TypeScript would have reported the clash plainly with "Duplicate identifier 'Comment'."
The second pitfall is the silent conflict in intersections. We saw it above: extends catches an incompatible property at the declaration, while & stays quiet there and pushes the problem to the place of use. When you build a type from other types and they share property names, an interface with extends is the safer path.
The third pitfall is thinking an alias creates a new type. Writing type Email = string and type Url = string doesn't stop you from passing an email address to a function that expects a URL:
type Email = string;
type Url = string;
function openLink(url: Url) {
console.log(`Opening ${url}`);
}
const email: Email = "ada@example.com";
openLink(email); // no error: both are just string
Aliases are there for readability; on their own they add no extra type safety.
A Small Experiment
★☆☆ Take the printUser and isAdult example from the start of the post and write it once with type User and once with interface User. In your editor, hover over the user parameter and compare the type shown in each case.
★★☆ Does it compile? Guess first: after writing interface Settings { theme: string; }, what happens if you add interface Settings { theme: number; } to the same file? Then try it and read the error message. Declaration merging has its limits too.
★★★ Take the Entity and Customer example and add a createdAt: string property to Customer, first with an interface and extends, then with a type and &. Which one reports the error, and where? In the intersection, what does the type of createdAt become: never, or something else?
In this post we gave types names and mentioned that type aliases can also express unions. In the next post we'll look exactly there: how we describe values that can be "either this or that" with union and literal types, and how TypeScript works with those possibilities.