Dataslope logoDataslope

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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":

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

  1. Declaration merging: Only interface supports it. If you're defining a public API and want users to be able to extend it, use interface.
  2. Unions and intersections: type can express union and intersection types (type Foo = A | B or type Bar = A & B). interface cannot.
  3. 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.
  4. Convention: Many teams use interface for object shapes and type for unions, primitives, and computed types. But this is not a hard rule.

Here's a side-by-side comparison:

Code Block
TypeScript 5.7

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.

Code Block
TypeScript 5.7

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):

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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.

Challenge
TypeScript 5.7
User Profile Formatter

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

QuestionSelect one

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

QuestionSelect one

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.

On this page