Objects and Interfaces
Structural typing for objects, interfaces, and the power of shape-based compatibility
Objects are the workhorses of JavaScript — key-value maps that bundle related data and behavior. TypeScript brings structure to these bundles by letting you declare the shape of an object: which properties it has, what type each property holds, whether a property is optional or readonly. The compiler then enforces this shape everywhere the object is used, catching typos, missing properties, and type mismatches before they reach production.
This chapter covers object type literals, interfaces, the
difference between interface and type, and how TypeScript's
structural type system makes objects compatible based on their shape,
not their declared name.
Object type literals
The simplest way to type an object is an object type literal — a curly-brace block listing the property names and their types:
What the compiler now knows: point has exactly two properties, x
and y, both numbers. If you write point.z, the compiler rejects it
— z does not exist on type { x: number; y: number }.
You can nest object types arbitrarily:
Object type literals are useful for one-off types (like function
parameters), but they quickly become verbose. That's where interface
and type aliases come in.
Interfaces: naming object shapes
An interface is a named object type. You declare it once, then reuse the name wherever you need that shape:
Interfaces make your code self-documenting: the name Point conveys
intent better than { x: number; y: number } repeated everywhere.
You can also define methods in interfaces:
What bugs does this prevent? If you forget to implement area, or if
area returns a string instead of a number, the compiler raises an
error. The interface acts as a contract between the object and its
consumers.
Optional properties
A property marked with ? is optional — it may be present or absent:
The type of bob.age is number | undefined, so you must narrow before
treating it as a pure number. This forces you to handle the "missing
property" case explicitly, preventing TypeErrors at runtime.
Readonly properties
A readonly property can be set once (during initialization) but not
reassigned:
readonly is a compile-time guarantee; it doesn't affect the
runtime. But it documents intent and prevents accidental mutations in your
codebase.
Index signatures
An index signature says "this object can have any property name
(matching a given pattern), and all such properties have type T":
The index signature [key: string]: string means "any property name (a
string) maps to a string value." This is useful for dictionaries or
configuration objects where you don't know the keys in advance.
You can also have [key: number]: T for array-like objects, or combine
an index signature with named properties:
Caveat: If you have an index signature, all named properties must
be compatible with it. In the example above, count is a number, which
matches the index signature [key: string]: number. If count were a
string, the compiler would reject it.
Interface extension
Interfaces can extend other interfaces, inheriting their properties:
Dog inherits name and age from Animal, then adds breed. This is
a powerful way to model hierarchies: you define base properties once, then
specialize as needed.
You can extend multiple interfaces at once:
This is called multiple inheritance (in a structural sense). The compiler merges all the properties from all the extended interfaces.
Declaration merging
One unique feature of interfaces (that type aliases lack) is
declaration merging. If you declare the same interface name twice,
TypeScript merges the declarations:
This is useful when augmenting third-party types (e.g., adding custom
properties to Window or NodeJS.Process). But it can also be
confusing, so use it sparingly. Declaration merging is one of the main
reasons to prefer interface over type for public APIs — if users need
to extend your types, they can re-declare the interface in their own
code.
interface vs type: when to use which
Both interface and type can describe object shapes, but they have
subtle differences:
- Declaration merging: Only
interfacesupports it. If you're defining a public API and want users to be able to extend it, useinterface. - Unions and intersections:
typecan express union and intersection types (type Foo = A | Bortype Bar = A & B).interfacecannot. - Performance: TypeScript's type checker is slightly faster with interfaces (because it can cache them by name), but the difference is negligible in most codebases.
- Convention: Many teams use
interfacefor object shapes andtypefor unions, primitives, and computed types. But this is not a hard rule.
Here's a side-by-side comparison:
For object shapes, the choice is mostly stylistic. But remember: if you
need unions or intersections, you must use type.
Structural typing: duck typing for the type system
TypeScript uses structural typing (also called "duck typing"): two types are compatible if they have the same shape, regardless of their name. This is different from nominal typing (used by Java, C#, etc.), where types are compatible only if they have the same name or explicit inheritance.
Even though Point2D and Vector2D are different names, they're
structurally identical (both have x: number and y: number), so
they're interchangeable. This is a key feature of TypeScript: types are
about shape, not name.
You can also assign an object with extra properties to a type with fewer properties (in most contexts):
The object user has both name and age, but Named only requires
name. So the assignment is valid. This is called excess property
checking in object literals, but it's looser when you assign an
existing variable.
Nested interfaces and complex shapes
Real-world objects often nest several layers deep. Interfaces handle this gracefully:
The compiler tracks the entire structure. If you misspell
emp.company.address.cty, you get a compile-time error, not a runtime
undefined.
Practice: a user profile system
Let's solidify these concepts with a challenge. You'll define interfaces for a user profile with optional properties and nested objects, then implement a function that pretty-prints the profile.
Define an interface Profile with the following properties: username: string, email: string, age?: number (optional), and settings: { theme: string; notifications: boolean } (nested object). Then implement the function formatProfile(profile: Profile): string that returns a formatted string like "Username: alice, Email: alice@example.com, Theme: dark". The test checks that the output contains the username and theme.
Check your understanding
What does the ? mean in an interface property like age?: number;?
The property is required but can be null
The property is optional and may be absent
The property is readonly
The property is deprecated
Given two interfaces with identical properties but different names, can you assign a value of one type to a variable of the other type?
No, TypeScript uses nominal typing
Yes, TypeScript uses structural typing
Only if one explicitly extends the other
Only if both are declared as type aliases
Summary
Objects are the building blocks of most TypeScript programs. By defining their shape with object type literals or interfaces, you tell the compiler what properties exist and what type each holds. The compiler enforces this shape at every access, catching typos and type mismatches before they reach production.
Interfaces support optional properties (?), readonly properties
(readonly), index signatures ([key: string]: T), extension
(extends), and declaration merging. TypeScript's structural type system
means compatibility is based on shape, not name — a powerful and flexible
model.
In the next chapter, Type Aliases and
Literals, we'll see how type aliases
differ from interfaces, and how literal types let you express specific
values in the type system.