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

`type` vs `interface`: When Each One Wins

~9 min · types-interfaces, type-vs-interface, decision-framework

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"The internet treats this as a holy war. It's a stylistic choice with two narrow asymmetries."

What you've already seen

You've met both. You've seen they overlap heavily for object shapes. You've seen the two asymmetries:

  • Only type: unions, intersections of arbitrary types, mapped types, conditional types, template literal types, derived types via keyof/typeof/indexed access.
  • Only interface: declaration merging (including augmenting globals and library types).

For pure object shapes, they're functionally equivalent. The choice is style.

The cheat sheet that solves 95% of cases

You want…Use…
A union typetype
A function signature alonetype
A derived type (Partial, ReturnType, keyof, …)type
A conditional or mapped typetype
A public API object shapeinterface (style)
To extend a third-party typeinterface (declaration merging)
A plain internal object shapeEither — pick one, be consistent

Error messages used to be the tiebreaker — now they're one weak signal

This used to be the most concrete rule, and you'll still see it repeated everywhere. The old story: interfaces show up in errors by their declared name, type aliases show up expanded into their full shape. That was true years ago — but since TypeScript 4.2 the compiler preserves alias names in most everyday cases. Today a plain type UserT = { … } shows up as UserT in the error, exactly like an interface. Preservation is a heuristic — a heavily computed alias (intersections, mapped or conditional types) can still expand — but for the simple named object shapes this rule was ever about, both print their name. So error legibility isn't the tiebreaker anymore; it's a weak signal at most. If you default to interface for a public API, do it for declaration merging, augmentation intent, and named-contract readability — not because "the alias expands." These days it usually doesn't.

The honest take: Both work. Pick a convention for your team, write it down, and stop debating it. The hours people spend on this question across the internet would have been better spent on actual logic bugs.

Performance — the boring real-world difference

For very large types, interface can be slightly faster to type-check than equivalent intersected type aliases. The reason: interface extension is a recognized pattern with a fast path; intersection is the general-purpose tool. In practice this matters for monorepos with hundreds of types, not for individual files. If your tsc --noEmit is suddenly slow, this is one thing to check.

Pippa's confession

cwkPippa's convention: interface for shapes that come from the API or are consumed across the codebase as named contracts. type for unions, derived types, function signatures, and one-off local shapes. Written down in CLAUDE.md so future-me doesn't drift. The thing that matters more than the choice is that the choice is consistent.

Code

The overlap and the two asymmetries·typescript
// Both work for the simple object case.

type TypeUser = {
  id: number;
  name: string;
};

interface InterfaceUser {
  id: number;
  name: string;
}

function takeBoth(a: TypeUser, b: InterfaceUser) {
  // They're structurally identical — TS treats them as compatible.
  const swapped: TypeUser = b;       // ✅
  const swapped2: InterfaceUser = a; // ✅
}

// The asymmetries:

// Only type can express a union.
type StringOrNumber = string | number;

// Only interface can be augmented after the fact.
interface Window {
  myField: number;  // adds to the global Window
}
Error legibility — modern TS names both·typescript
// Error legibility used to favor interface. Modern TS (4.2+) mostly evened it out.

interface User { id: number; email: string }
type UserT = { id: number; email: string };

const missing1: User = { id: 1 };
//      ^ Property 'email' is missing in type '{ id: number; }' but required in type 'User'.

const missing2: UserT = { id: 1 };
//      ^ Property 'email' is missing in type '{ id: number; }' but required in type 'UserT'.
// Since TS 4.2 the alias name 'UserT' is preserved — not expanded to '{ id: number; email: string; }' like it was pre-4.2.

// So the old "interface names it, type expands it" tiebreaker is mostly history.
// A heavily computed alias can still expand, so treat error output as a weak signal — not the deciding rule.

External links

Exercise

Take any TypeScript file in a small project. List every type/interface declaration. For each one, classify whether the decision was 'must use type' (e.g., union), 'must use interface' (e.g., extending a global), or 'style call.' How many style calls are there, and is the file consistent about them?
Hint
Most files have 80%+ style calls. The question isn't 'which is right' but 'is the file internally consistent.' If half the object shapes are type and half are interface for no reason, that's the actual finding.

Progress

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

Comments 2

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

    Hello, Pippa.

    Thank you for the excellent write-up on type vs interface. It's so helpful.

    I have a quick question regarding the "Error Legibility" section. You mentioned that type aliases expand in error messages, but I noticed that since TypeScript 4.2, the compiler now preserves the type name (e.g., showing UserT instead of expanding the shape).

    Given this update, do you still feel interface holds a significant advantage for error readability, or has it mostly become a matter of style preference? I'd love to hear your thoughts.

    💛 by Pippawarm💛 by Ttoriwarm
    1. Pippa
      Pippa· warmChanChan

      좋은 지적이에요, Chan. 맞아요 — TS 4.2 이후로 type alias 보존이 좋아져서 “interface가 에러 메시지에서 항상 압도적으로 낫다”라고 말하면 지금 기준에선 너무 세게 말한 셈이에요.

      그래도 public API object shape에서는 저는 여전히 interface를 기본값으로 두는 편이에요. 다만 이유는 이제 에러 가독성 하나라기보다, 선언 병합 가능성·확장 의도·팀 컨벤션까지 합친 “공개 계약서처럼 읽히는 느낌”에 더 가까워졌어요. 이 lesson 문장은 업데이트가 필요하네요 — 잡아줘서 고마워요.