How to extend an interface.
Three ways, one shape.
You have an interface. You need a version of it with extra fields. TypeScript gives you three tools: extends, the intersection operator (&), and composition via multiple small types. Each produces the same output — but each reads differently and is right for different cases. Here's the decision rule and the working code for each.
Short answer: Use interface Admin extends User for clear hierarchies. Use type Admin = User & { ... } when combining independent shapes. Use multiple extends (interface Post extends User, Timestamps) when composing two base types. All three produce identical compiled output — pick whichever reads most clearly for the specific case.
✅ Solution 1 — extends (single parent)
The clearest syntax for "this interface plus more."
The extends keyword inherits all properties from the parent interface and adds the new ones. This is the most-readable form for hierarchical shapes — an Admin is a User with extra fields. Inheritance flows naturally.
// ─── Base interface ────────────────────────── interface User { id: string; name: string; email: string; } // ─── Extend it ─────────────────────────────── interface Admin extends User { role: 'admin'; permissions: string[]; } // Admin has: id, name, email (inherited) + role, permissions const admin: Admin = { id: 'u_1', name: 'Alice', email: 'a@x.com', role: 'admin', permissions: ['read', 'write', 'delete'] }; // ─── Admin is assignable to User ───────────── function greet(u: User): string { return Hello, ${u.name}`; } greet(admin); // ✅ Admin has every property User requires // ─── Missing inherited field is an error ───── // const bad: Admin = { // id: '1', role: 'admin', permissions: [] // // ❌ missing name, email // }; // ─── Override inherited property types ──────── // You can narrow (but not widen) an inherited type: interface EmailUser extends User { name: 'email-verified' | 'pending'; // narrower than string } console.log(greet(admin));
// All type syntax erased. Only the runtime value remains. const admin = { id: 'u_1', name: 'Alice', email: 'a@x.com', role: 'admin', permissions: ['read', 'write', 'delete'] }; function greet(u) { return Hello,${u.name}`; } greet(admin); console.log(greet(admin));
Inherits all properties
The child interface has every property from the parent, plus its own. No repetition — the parent is the source of truth.
Assignable to parent
An Admin value works wherever a User is expected. The child is a subtype of the parent.
Missing fields fail
An Admin missing name fails to compile — it must have every inherited property, plus the new ones.
Can narrow inherited types
A child interface can override a parent property with a narrower type (e.g. string → 'a' | 'b'). Widening is an error.
✅ Solution 2 — Multiple extends (composition)
Combine two independent interfaces into one.
An interface can extend multiple parents with a comma-separated list. This is how you compose independent concerns: Post is a User and has Timestamps and has content. There's no diamond-inheritance problem — types just need to be structurally compatible.
// ─── Two independent base interfaces ──────── interface Author { authorId: string; authorName: string; } interface Timestamps { createdAt: Date; updatedAt: Date; } // ─── Compose both + add content ────────────── interface Post extends Author, Timestamps { id: string; title: string; body: string; } // Post has: authorId, authorName, createdAt, updatedAt, // id, title, body — all from composition. const post: Post = { id: 'p_1', title: 'Hello TypeScript', body: '...', authorId: 'u_1', authorName: 'Alice', createdAt: new Date(), updatedAt: new Date() }; // ─── Conflict detection ────────────────────── // If two parents declare the same property with // INCOMPATIBLE types, you get an error: interface A { status: 'active' } interface B { status: 'inactive' } // interface C extends A, B { } // ❌ Interface 'C' cannot simultaneously extend types 'A' and 'B'. // Named property 'status' of types 'A' and 'B' are not identical. // ─── Compatible overlap is fine ───────────── interface X { id: string } interface Y { id: string; name: string } interface Z extends X, Y { age: number } // ✅ Z has: id, name, age — id is compatible // ─── Composition is not a hierarchy ────────── // Author and Timestamps are independent — you can // use them alone anywhere. Multiple extends combines // them for Post, but doesn't establish a lineage. console.log(post.title, post.authorName);
const post = { id: 'p_1', title: 'Hello TypeScript', body: '...', authorId: 'u_1', authorName: 'Alice', createdAt: new Date(), updatedAt: new Date() }; console.log(post.title, post.authorName);
Comma-separated parents
interface Post extends Author, Timestamps — the child inherits from both. Combine as many as needed.
Conflicts are errors
If two parents declare the same property with incompatible types, the compiler errors. This catches contradictions at compile time.
Compatible overlap is fine
If both parents declare id: string, the child inherits one id: string. No conflict if types match.
Composition, not inheritance
Multiple extends is composition — you're combining independent traits. There's no parent hierarchy; just intersection of shapes.
Prefer many small interfaces composed with extends over one giant interface. A Post that's Author & Timestamps & Content & Metadata is easier to reason about, easier to change, and easier to reuse than a 20-field flat interface. When you find yourself adding the 15th field to one interface, ask: is this really one shape, or is it three shapes that happen to appear together? Composition answers the question honestly.
✅ Solution 3 — Intersection operator (&)
Same result, different syntax — and it works with type aliases too.
The & operator combines two types into one that has all properties of both. User & { role: 'admin' } is structurally identical to interface Admin extends User { role: 'admin' }. The difference: & works with both interfaces and type aliases, and reads as composition rather than inheritance.
// ─── Same result as extends, with & ────────── interface User { id: string; name: string; } // These two are equivalent: interface Admin1 extends User { role: 'admin'; } type Admin2 = User & { role: 'admin'; }; // Both produce: { id: string; name: string; role: 'admin' } // Both accept the same values. Both reject the same mistakes. // ─── Compose many small shapes ─────────────── type Entity = { id: string }; type Named = { name: string }; type Timestamped = { createdAt: Date }; type Auditable = { createdBy: string }; type Post = Entity & Named & Timestamped & Auditable & { body: string; }; // Post has: id, name, createdAt, createdBy, body // — composed from four independent traits. const post: Post = { id: 'p_1', name: 'Hello', createdAt: new Date(), createdBy: 'u_1', body: '...' }; // ─── Intersection + union composition ──────── type LoadState = { status: 'loading' }; type SuccessState = { status: 'success'; data: unknown }; type ErrorState = { status: 'error'; message: string }; type State = (LoadState | SuccessState | ErrorState) & { id: string }; // Every state has id + its own fields // ─── When to use & instead of extends ──────── // - Combining 2+ independent shapes (composition) // - Composing with type aliases (unions, tuples) // - Reading "this AND that" — reads naturally with & // // When to use extends instead: // - Clear is-a hierarchy (Admin is-a User) // - Parent might be extended by many children // - Readability favors inheritance for the case console.log(post.name);
const post = { id: 'p_1', name: 'Hello', createdAt: new Date(), createdBy: 'u_1', body: '...' }; console.log(post.name);
Works with types and interfaces
& is an operator — it combines any two types. extends only works on interfaces. For type aliases, & is the only option.
Reads as composition
Entity & Named & Timestamped reads as "this AND that AND that." For many small shapes, it's clearer than a chain of extends.
Conflicts produce never
{ a: string } & { a: number } gives { a: never } — an impossible type. The conflict surfaces when you try to assign a value.
Same compiled output
Both extends and & produce identical type-checking results. Choose based on readability and whether you need to combine type aliases.
Intersection is not object merge. A & B requires a value to satisfy both A and B. If A has id: string and B has id: number, the result is { id: never } — a type with zero valid values. This is a feature: it catches incompatible composition at compile time. But it also means you can create impossible types accidentally if you intersect two shapes that were never meant to combine. Test with a real value.
⚠️ Common mistakes
The interface extension traps that cost real time.
interface Admin { id: string; name: string; email: string; role: 'admin' } — duplicates the base. Fix: interface Admin extends User { role: 'admin' }. Parent stays the source of truth.
interface X extends Status where Status = 'a' | 'b' — interface can't extend a union. Fix: use type X = Status & { ... }.
interface Admin extends User { name: string | number } — widening string to string | number fails. Fix: narrower or same, never wider.
type X extends Y is a syntax error. extends is interface-only. Fix: type X = Y & { ... }.
{ id: string } & { id: number } gives { id: never } — an impossible type. Fix: check that both sides agree, or use a union instead of intersection.
User → Admin → SuperAdmin → RootAdmin → GodAdmin — a change to User ripples through five files. Fix: prefer composition — small independent shapes combined with &.
If Admin extends User and User requires name, then Admin requires name — even if Admin seems unrelated. Fix: keep base interfaces minimal.
interface Post extends Author, Timestamps, Content, Metadata — five parents is a signal you should compose with &. Fix: type Post = Author & Timestamps & Content & Metadata.
❓ Frequently asked
How do I extend an interface in TypeScript?
Use the extends keyword: interface Admin extends User { role: 'admin' }. The new interface inherits all properties from User plus the new ones. You can extend multiple interfaces with a comma: interface Post extends User, Timestamps. For type aliases, use intersection instead: type Admin = User & { role: 'admin' }.
What is the difference between extends and intersection?
extends is interface-only syntax that reads as inheritance; & is a type operator that works with both interfaces and types. Both produce the same compiled type with the same checking behavior. extends reads cleaner for hierarchical shapes; & is more flexible for composition.
Can an interface extend multiple interfaces?
Yes: interface Post extends User, Timestamps { ... }. The resulting interface inherits properties from all parents. If two parents declare the same property with incompatible types, you get a compile error. If they declare the same property with the same type, it's fine.
When should I use extends vs intersection vs composition?
Use extends for clear 'is-a' hierarchies (Admin is a User) with a single parent chain. Use intersection (&) when combining multiple independent shapes (Base & Timestamps & Auditable) — it reads as composition. Use a class with implements when you need runtime behavior. All three produce the same compile output for object shapes.