Skip to content

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.

type can describe any type, not just objects:

type Difficulty = 'easy' | 'medium' | 'hard'; // union of literals
type ScreenId = 'start-screen' | 'result-screen'; // union of literals
type NullableScore = HighScore | null; // union with null
type Callback = (score: number) => void; // function signature
type Pair = [string, number]; // tuple
type StringOrNumber = string | number; // primitive union

None 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 too

The 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.

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 shapes
interface TriviaQuestion { /* ... */ }
interface HighScore { /* ... */ }
interface ApiResponse { /* ... */ }
// type — everything else
type 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.

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.

Review types.ts and answer these questions in comments at the top of the file:

  1. Which declarations should be interface? (Data models — shapes that describe domain objects.)
  2. Which should be type? (Unions, literal unions, tuples, or anything that cannot be an interface.)
  3. Rewrite any type aliases that describe plain object shapes as interface declarations (unless they use features only type supports).
  4. Add a type QuizState that is a discriminated union (preview for Module 06):
    type QuizState =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'active'; question: TriviaQuestion; index: number; total: number }
    | { status: 'done'; score: number; total: number };
    This must be a type — why?
  • type can describe any type: unions, tuples, function signatures, primitives, intersections. interface can only describe object shapes.
  • Interfaces support declaration merging; type aliases do not.
  • interface extends catches property conflicts at the declaration; & intersection silently produces never for conflicting properties.
  • Use interface for named data models; use type for unions, tuples, function types, and everything else.
  • Consistency within a project matters more than the specific choice between them.