Skip to content

Extending Interfaces and Intersecting Types

As a codebase grows, types that once felt self-contained start sharing properties. Copying those properties into every type that needs them is the wrong answer — it creates drift when one copy changes and the others do not. TypeScript gives you two composition tools: extends for interfaces, and & for type aliases.

An interface can extend another interface, inheriting all of its properties:

interface BaseQuestion {
question: string;
category: string;
difficulty: Difficulty;
}
interface TriviaQuestion extends BaseQuestion {
correctAnswer: string;
allAnswers: string[];
}

TriviaQuestion now has five properties: the three from BaseQuestion plus its own two. A value of type TriviaQuestion must satisfy all five:

const q: TriviaQuestion = {
question: 'What planet is closest to the Sun?',
category: 'Science & Nature',
difficulty: 'easy',
correctAnswer: 'Mercury',
allAnswers: ['Mercury', 'Venus', 'Mars', 'Earth'],
};

An interface can extend more than one parent at once:

interface WithId {
id: string;
}
interface WithTimestamp {
createdAt: string;
}
interface StoredQuestion extends WithId, WithTimestamp {
question: string;
correctAnswer: string;
}

StoredQuestion inherits id and createdAt alongside its own properties. This pattern lets you build small, focused interfaces for cross-cutting concerns — identifiers, timestamps, metadata — and mix them into the types that need them.

An extending interface can narrow (but not widen) an inherited property:

interface BaseQuestion {
difficulty: string;
}
interface TriviaQuestion extends BaseQuestion {
difficulty: Difficulty; // ✓ — 'easy' | 'medium' | 'hard' is narrower than string
}

Widening an inherited property is an error:

interface Specific {
difficulty: Difficulty;
}
interface Loose extends Specific {
difficulty: string; // Error — string is wider than Difficulty
}

Type aliases use & to compose, which you saw in Module 02. The result is a type that requires all properties from all operands:

type WithId = { id: string };
type WithScore = { score: number; total: number };
type ScoredEntry = WithId & WithScore;
// equivalent to: { id: string; score: number; total: number }

& and extends produce structurally equivalent results for simple object shapes. The difference is syntax and intent: extends reads as “is a kind of”, & reads as “has everything from both”.

For object types:

  • Use extends with interface when the relationship is conceptual inheritance — a TriviaQuestion is a BaseQuestion with more detail.
  • Use & with type when you are mechanically combining two independent shapes — a score record that also needs a timestamp.

In practice, both work. The preference in most TypeScript codebases is interface extends for named data models and & for ad-hoc combinations.

AceIt: RawQuestion and TriviaQuestion share a base

Section titled “AceIt: RawQuestion and TriviaQuestion share a base”

RawQuestion (the API shape) and TriviaQuestion (the app shape) share three fields: question, category, and difficulty. You can factor those into a shared base:

interface BaseQuestion {
question: string;
category: string;
difficulty: Difficulty;
}
interface RawQuestion extends BaseQuestion {
type: string;
correct_answer: string;
incorrect_answers: string[];
}
interface TriviaQuestion extends BaseQuestion {
readonly correctAnswer: string;
readonly allAnswers: string[];
}

Both interfaces now document explicitly that they describe the same underlying concept. If a field shared between them changes — say, difficulty becomes a more complex type in a future API version — you change BaseQuestion once.

In types.ts:

  1. Extract a BaseQuestion interface with question: string, category: string, and difficulty: Difficulty.
  2. Rewrite RawQuestion to extend BaseQuestion, adding only the fields unique to the API shape.
  3. Rewrite TriviaQuestion to extend BaseQuestion, adding readonly correctAnswer and readonly allAnswers.
  4. Create a type TimestampedHighScore = HighScore & { recordedAt: number } using intersection. Confirm a variable of that type requires all four fields.
  • interface B extends A inherits all of A’s properties into B.
  • An interface can extend multiple parents: interface C extends A, B.
  • Extending interfaces can narrow inherited properties but not widen them.
  • Type alias intersection (&) produces a structurally equivalent result for object shapes.
  • Use extends for conceptual inheritance; use & for mechanical combination of independent shapes.