C.W.K.
Stream
Lesson 02 of 06 · published

`interface`: When Names Matter

~10 min · types-interfaces, interface, extends, declaration-merging

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"interface is type for people who love the word interface. Mostly."

What interface does well

interface declares a named object shape. interface User { id: number; name: string } creates a type User with two members. From the consumer's perspective, this is nearly identical to a type alias of the same shape.

The places interface earns its name:

1. extends reads naturally for nominal-feeling hierarchies. interface AdminUser extends User { permissions: string[] } communicates intent the way a class hierarchy does. The equivalent type uses intersection (type AdminUser = User & { permissions: string[] }) which is more powerful but reads less obviously.

2. Declaration merging. Two interface User declarations in the same scope merge into one. This is how libraries augment global types (TypeScript itself does this for lib.dom.d.ts). It's also how you add custom properties to Window, Express.Request, or any third-party interface.

3. Implementing classes. A class can declare class MyUser implements User. Both type and interface work here, but implements reads cleanly with interface and matches the Java/C# muscle memory many developers have.

Declaration merging — the killer feature

This is the unique power of interface. The same name can be declared more than once, and the declarations merge:

interface Box { width: number }
interface Box { height: number }
// Box now has BOTH width and height

It feels weird the first time you see it. The use case becomes obvious when you're augmenting a library's types or extending a global. declare global { interface Window { myApp: MyAppGlobal } } adds myApp to window without you owning the original Window declaration. Type aliases can't do this — they error on duplicate names.

Where interface struggles

Anything that isn't an object shape: unions, intersections of non-objects, mapped types, conditional types, function types in isolation. For all of these, type aliases are the only option. The call-signature syntax for function-shaped interfaces (interface F { (x: number): string }) is technically valid but rarely seen.

The actual rule: Use interface when you want named, extensible, object-shaped types — especially public API surfaces and types other libraries might augment. Use type for everything else. The two are not in competition; they're tools with overlapping but distinct sweet spots.

Pippa's confession

cwkPippa's backend types come from Pydantic on the Python side; the frontend interface declarations mirror them by hand (the Pippa Coop System has a future task to generate them via codegen). I use interface for those request/response shapes because they're hierarchical and read like contracts — exactly what interface is best at. Everything else (unions, derived types, function signatures) is type.

Code

interface for hierarchical, named shapes·typescript
// interface — named, extensible object shapes.

interface User {
  id: number;
  name: string;
  email: string | null;
}

// Extends — the natural way to build hierarchies.
interface AdminUser extends User {
  permissions: string[];
  canEditAll: boolean;
}

// Multiple extends.
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface AuditedAdmin extends AdminUser, Timestamped {
  auditTrail: string[];
}

// Class implementing an interface.
class MyUser implements User {
  constructor(
    public id: number,
    public name: string,
    public email: string | null,
  ) {}
}
Declaration merging — augmenting global and library types·typescript
// Declaration merging — the only thing `type` can't do.

interface Box {
  width: number;
}
interface Box {
  height: number;
}

const b: Box = { width: 100, height: 200 };       // both fields required

// The real-world use: augmenting globals.
declare global {
  interface Window {
    myApp: {
      version: string;
      pippa: { hello: () => void };
    };
  }
}

window.myApp.pippa.hello();                       // ✅ now typed

// And the Express.js pattern: extending Request.
// (In a .d.ts file in your project root.)
declare namespace Express {
  interface Request {
    user?: { id: number; role: 'admin' | 'user' };
  }
}

External links

Exercise

Create an interface Vehicle { wheels: number; brand: string }. Then create an interface Car extends Vehicle { doors: number }. Now in a separate file (or below), declare interface Car { color: string } — does it merge? What's now the full shape of Car? Then try the same trick with type aliases and see what happens.
Hint
The declaration merging makes Car require all four fields. With type aliases, the second declaration would be a duplicate-identifier error. That's the difference, demonstrated.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.