Interface vs Type Alias — When to Use Each
Both interface and type can describe object shapes. The overlap is significant — for most object types, either works. But there are concrete differences, and understanding them leads to a consistent, readable codebase.
What only type can do
Section titled “What only type can do”type can describe any type, not just objects:
type Difficulty = 'easy' | 'medium' | 'hard'; // union of literalstype ScreenId = 'start-screen' | 'result-screen'; // union of literalstype NullableScore = HighScore | null; // union with nulltype Callback = (score: number) => void; // function signaturetype Pair = [string, number]; // tupletype StringOrNumber = string | number; // primitive unionNone of these can be written with interface. Interfaces can only describe object shapes. If you need to name a union, a tuple, a function type, or a primitive, type is your only option.
What only interface can do — declaration merging
Section titled “What only interface can do — declaration merging”Interfaces support declaration merging: two interface declarations with the same name in the same scope merge into one:
interface Quiz { score: number;}
interface Quiz { total: number;}
// Result: interface Quiz { score: number; total: number; }const q: Quiz = { score: 7, total: 10 }; // ✓Type aliases do not merge — declaring type Quiz twice is an error. Declaration merging is mainly used to extend third-party types (like extending the Window interface to add custom properties) and is rarely something you need in application code. For your own types, merging is usually a sign the two declarations should simply be combined into one.
Extends vs intersection — a subtle difference
Section titled “Extends vs intersection — a subtle difference”Both extends and & compose object types, but they handle property conflicts differently:
interface A { value: string | number; }interface B extends A { value: string; } // ✓ — narrowing is allowed
type A = { value: string | number };type B = A & { value: string }; // ✓ — intersection narrows tooThe difference appears when types conflict in an irreconcilable way:
interface A { value: string; }interface B extends A { value: number; } // Error — can't narrow string to number
type A = { value: string };type B = A & { value: number }; // No error at declaration, but value becomes 'never'interface extends catches the conflict at the declaration site — a clear error. The & intersection silently produces a never property, which only surfaces when you try to assign a value to it. For detecting composition errors early, extends is safer.
The decision rule
Section titled “The decision rule”A simple rule that works for most codebases:
Use interface for object shapes that describe data models.
TriviaQuestion, RawQuestion, ApiResponse, HighScore — named objects that represent real things in the domain. Interfaces are extendable, composable, and produce clear error messages.
Use type for everything else.
Unions, tuples, function types, aliases for primitives, intersections of unrelated shapes, and any type that is not purely an object.
// interface — object shapesinterface TriviaQuestion { /* ... */ }interface HighScore { /* ... */ }interface ApiResponse { /* ... */ }
// type — everything elsetype Difficulty = 'easy' | 'medium' | 'hard';type ScreenId = 'start-screen' | 'loading-screen' | 'question-screen' | 'result-screen';type QuizState = { status: 'idle' } | { status: 'active'; question: TriviaQuestion };type Callback = (score: number, total: number) => void;This is the pattern used in AceIt’s types.ts and in the majority of professional TypeScript codebases.
When you see both in the wild
Section titled “When you see both in the wild”In real codebases you will see both used for object shapes — often inconsistently. The TypeScript team itself uses both in the standard library. The rule above is a guideline, not a law. What matters most is being consistent within a project: pick a convention and follow it.
Exercise
Section titled “Exercise”Review types.ts and answer these questions in comments at the top of the file:
- Which declarations should be
interface? (Data models — shapes that describe domain objects.) - Which should be
type? (Unions, literal unions, tuples, or anything that cannot be an interface.) - Rewrite any
typealiases that describe plain object shapes asinterfacedeclarations (unless they use features onlytypesupports). - Add a
type QuizStatethat is a discriminated union (preview for Module 06):This must be atype QuizState =| { status: 'idle' }| { status: 'loading' }| { status: 'active'; question: TriviaQuestion; index: number; total: number }| { status: 'done'; score: number; total: number };type— why?
typecan describe any type: unions, tuples, function signatures, primitives, intersections.interfacecan only describe object shapes.- Interfaces support declaration merging; type aliases do not.
interface extendscatches property conflicts at the declaration;&intersection silently producesneverfor conflicting properties.- Use
interfacefor named data models; usetypefor unions, tuples, function types, and everything else. - Consistency within a project matters more than the specific choice between them.