Interfaces
An interface is a named description of an object's shape — the properties it must have and their types. It exists only at compile time; like everything else in the Type System Fundamentals topic, it's fully erased before runtime.
🎯 What you'll learn
- What an Interface Is
- Optional and Readonly Properties
- Method Signatures
- Extending Interfaces
- Declaration Merging
1What an Interface Is
An interface is a named description of an object's shape — the properties it must have and their types. It exists only at compile time; like everything else in the Type System Fundamentals topic, it's fully erased before runtime.
interface User {
id: number;
name: string;
email: string;
}
function printUser(user: User) {
console.log(`${user.name} <${user.email}>`);
}2Optional and Readonly Properties
interface Config {
retries?: number; // optional — may be omitted entirely
readonly apiKey: string; // can be set once, never reassigned after
}
const config: Config = { apiKey: "abc123" }; // retries omitted — fine
config.apiKey = "xyz789"; // ❌ error: apiKey is readonlyreadonly is a compile-time-only restriction — like all TypeScript features, nothing stops mutation at runtime if the value is accessed through an untyped or any reference.
3Method Signatures
interface Greeter {
greet(name: string): string; // method shorthand syntax
farewell: (name: string) => string; // property holding a function type
}Both describe a callable member, but they are checked slightly differently by the compiler (full detail in the "Under the Hood" section) — the shorthand greet(...) form is the more common, idiomatic style for interface methods.
4Extending Interfaces
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const rex: Dog = { name: "Rex", breed: "Labrador" }; // must satisfy BOTH shapesAn interface can extend multiple interfaces at once — combining every required member from each:
interface Swimmer { swim(): void; }
interface Flyer { fly(): void; }
interface Duck extends Swimmer, Flyer {
quack(): void;
}5Declaration Merging
Unlike almost every other declaration in TypeScript, multiple interface declarations with the same name automatically merge into one combined interface, rather than causing a duplicate-declaration error.
interface Window {
title: string;
}
interface Window {
size: { width: number; height: number };
}
// merged result — Window now requires BOTH title and size
const w: Window = { title: "My App", size: { width: 800, height: 600 } };This is intentional and widely used for augmenting types you don't own — e.g. adding a custom property to the global Window or process.env types from a library, without modifying that library's source.
6Index Signatures
interface StringDictionary {
[key: string]: string; // any string key maps to a string value
}
const colors: StringDictionary = { primary: "blue", secondary: "green" };
colors.tertiary = "red"; // ✅ any string key is valid7Interfaces vs Type Aliases (Preview)
Both can describe object shapes, and are interchangeable for most everyday use — the full comparison (including why interfaces merge and type aliases don't) is the Type Aliases topic. The short version: prefer interface for object shapes meant to be extended or augmented, and type for unions, tuples, and other non-object-shape constructs interfaces can't express at all.
Under the HoodMerging and the Method-vs-Property Distinction▾
Declaration merging is literal member-list concatenation
When the checker encounters two interface Window declarations, it doesn't pick one or error — it builds a single internal symbol for Window and concatenates both declarations' member lists into one combined shape, checked as a unit from then on. This is a real, deliberate compiler feature — not a coincidence of how modules work — which is why it works even across separate files compiled together (a common pattern: a library ships a base interface, and your project's own .d.ts file adds a merging declaration to extend it without touching the library's source).
interface Window { title: string } → member list: [title]
interface Window { size: {...} } → member list: [title, size] (merged, not replaced)Methods are checked bivariantly; property function types are not
This is one of TypeScript's most subtle, deliberate design choices:
interface Handler {
onEvent(e: MouseEvent): void; // method shorthand — checked BIVARIANTLY
onEventProp: (e: MouseEvent) => void; // property function type — checked STRICTLY (contravariantly)
}
class SpecificHandler implements Handler {
onEvent(e: Event): void {} // ✅ allowed — narrower parameter type, bivariant check permits it
onEventProp = (e: Event) => {}; // ❌ error — strict function type checking rejects this
}Structurally, accepting a narrower parameter type than required is technically unsound (a function expecting a specific MouseEvent might not handle every generic Event) — but TypeScript deliberately allows it for method shorthand syntax specifically, because this exact pattern (an override accepting a more specific event type than the base signature) is extremely common in real-world DOM/UI code, and strict-everywhere checking here would reject huge amounts of valid, safe-in-practice code. Property-syntax function types don't get this exception — they're checked with full strictness. This asymmetry is intentional, documented compiler behavior, not an inconsistency.
Excess property checks apply to interfaces the same way
Since interfaces describe object shapes, they participate in the Type System Fundamentals topic's excess-property-check special case identically to inline object type annotations — a fresh object literal assigned directly to an interface-typed variable is checked for unknown properties; the same object passed through a variable first is not.
⚠️ Common Pitfalls
- Expecting readonly to prevent runtime mutation — it's compile-time only;
Object.freeze()is the actual runtime enforcement mechanism, and the two are unrelated. - Being surprised by declaration merging — accidentally reusing an interface name in the same scope silently merges instead of erroring, which can hide what looks like a duplicate-declaration bug.
- Assuming method shorthand and property function types are equivalent — the bivariance difference in section 8 means they behave differently for subtyping and
implementschecks, even though they look interchangeable at first glance. - Forgetting
extendscombines requirements, not options — an interface extending two others must satisfy the UNION of their required members, not either one alone.
📋 Quick Reference
interface Name { prop: T; opt?: T; readonly ro: T; method(): T }
interface A extends B, C {} → combines required members
interface X {} + interface X {} → MERGE, not an error
[key: string]: T → index signature🎤 Interview Q&A
Q1What are interfaces used for?core▾
Describing object shapes and contracts for classes: properties, optional (?) and readonly members, method signatures, index signatures, and extension via extends.
- Can classes implement interfaces? Yes, with
implements; checked structurally.
Q2Interface vs type alias?core▾
Both describe object shapes. Interfaces support declaration merging and extends and often give clearer error messages; type aliases can express unions, intersections, mapped and conditional types. Common guidance: interfaces for public object contracts, types for everything else.
- What's declaration merging? Multiple interface declarations with the same name combine.
Q3How do you type dictionaries?core▾
Index signatures ({ [key: string]: number }) or Record<string, number>; with noUncheckedIndexedAccess, lookups return number | undefined. For known keys, use unions: Record<'a' | 'b', number>.
- Map vs Record? Map for dynamic keys at runtime.
Q4How does module augmentation use interfaces?senior▾
Declaration merging lets you extend third-party types, e.g. adding a user property to Express's Request interface via declare module 'express-serve-static-core'.
- Global augmentation?
declare global { interface Window { … } }.
Q5How do readonly and optional modifiers work?core▾
readonly prevents reassignment through that type (compile-time only, shallow); ? makes a property optional (T | undefined). exactOptionalPropertyTypes distinguishes missing from explicitly undefined.
- Deep readonly? Recursive mapped types or libraries.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.