Model Real Data with TypeScript Objects

Apply object types, interfaces, optional properties, and readonly to model a real dataset: a library of books. Build types step by step and see how the compiler guides correct data shapes.

7 min read

TypeScript object types are at their best when you use them to model real data. Instead of describing abstract Point or User shapes, you describe the actual data your application works with: products, orders, articles, events. The compiler then checks that every object you create matches the model.

This article walks through modeling a small library of books. You will see how to layer interfaces, optional properties, readonly fields, and nested types to build a model that is both precise and practical.

Step 1: The core model

A data model should start small. Define only the properties every book has, then grow the shape as new requirements appear. That means a title, an author, and a publication year.

typescripttypescript
interface Book {
  title: string;
  author: string;
  year: number;
}
 
const book: Book = {
  title: "The TypeScript Handbook",
  author: "The TS Team",
  year: 2023,
};

With just these three properties, the compiler already catches several mistakes. If you forget year, it tells you the property is missing. If you pass a string for year, it tells you a number is expected. If you misspell title as titel, it tells you the property does not exist on Book.

Step 2: Adding optional properties

Real books have data that may or may not be relevant. An ISBN, a page count, a cover image URL. Mark these as optional.

typescripttypescript
interface Book {
  title: string;
  author: string;
  year: number;
  isbn?: string;
  pages?: number;
  coverUrl?: string;
}

Now a book can be created with just the core three properties. If you provide an ISBN, it must be a string. If you omit it, the compiler treats it as potentially undefined when read. Here are two valid books, one minimal and one with every optional field:

typescripttypescript
const shortBook: Book = { title: "Haiku Collection", author: "Matsuo Basho", year: 1690 };
 
const fullBook: Book = {
  title: "The TypeScript Handbook",
  author: "The TS Team",
  year: 2023,
  isbn: "978-0-123456-78-9",
  pages: 420,
  coverUrl: "https://example.com/cover.png",
};

The shortBook uses only the required fields. The fullBook includes every optional property. Both compile because each required field is present and each provided value has the correct type.

Step 3: Readonly and nested types

Some properties should not change after creation. An ID from a database, for example. Other properties are objects themselves. Use nested interfaces and readonly to model both.

This version replaces the Book interface from Step 2 rather than adding to it. If you keep both versions in the same file, TypeScript merges the repeated declarations and rejects the result, because author cannot be both string and Author at once.

typescripttypescript
interface Author {
  readonly id: string;
  name: string;
}
 
interface Book {
  title: string;
  author: Author;
  year: number;
  readonly isbn?: string;
}

The Author type has a readonly id that cannot be changed after the object is created. The Book type has a readonly isbn that is also locked after assignment. The compiler will reject any attempt to overwrite a readonly property. Here is a book with a nested author, followed by a failed attempt to change its ISBN:

typescripttypescript
const book: Book = {
  title: "Effective TypeScript",
  author: { id: "auth-42", name: "Dan Vanderkam" },
  year: 2019,
};
 
book.isbn = "something-else";
// Error: Cannot assign to 'isbn' because it is a read-only property.

The compiler prevents overwriting the ISBN at compile time. You can still read book.author.name. But Author's own properties are mutable unless you mark them readonly too.

Step 4: Building the library

Now combine individual books into a library with an array of Book objects and a containing interface:

typescripttypescript
interface Library {
  name: string;
  books: Book[];
  readonly createdAt: Date;
}

You now have a fully typed library. You can build an instance with a name, a collection of books, and a creation timestamp. Then query it with functions that the compiler fully type-checks:

typescripttypescript
const myLibrary: Library = {
  name: "My Collection",
  books: [shortBook, fullBook, book],
  createdAt: new Date(),
};

Every property access is checked. You get autocomplete at every dot.

Step 5: What the compiler caught

A model this rich prevents a class of bugs common with plain JavaScript.

Missing required properties are caught at creation. Optional properties force null checks before use. Readonly prevents accidental overwrites. Nested types ensure deep access is safe.

The model also serves as documentation. A new developer reading the Book interface knows what properties exist, which are required, and which can be missing.

For more on the building blocks used here, see object types in TypeScript and optional properties in TypeScript.

Rune AI

Rune AI

Key Insights

  • Name your data shapes with interfaces for clarity and reuse.
  • Mark properties as optional when they may not always be present.
  • Use readonly for properties that should not change after creation.
  • Nest interfaces to match the natural hierarchy of your data.
  • The compiler catches missing properties, wrong types, and typos before the code runs.
RunePowered by Rune AI

Frequently Asked Questions

Should I use an interface or a type alias for data models?

Interfaces are a natural fit for data models because they are open (support declaration merging) and produce clear error messages. But type aliases work equally well. Choose one and stay consistent across the project.

How do I handle data that might be missing or incomplete?

Mark those properties as optional with a question mark. The compiler will force callers to check for undefined before using the value. For data that comes from an API, consider runtime validation in addition to compile-time types.

Can I nest interfaces to model complex data?

Yes. An interface property can itself be an interface, a type alias, or an inline object type. Nesting lets you model real-world data with the same hierarchy it has in practice.

Conclusion

Modeling real data with TypeScript objects is about translating real-world entities into typed shapes. Start with the core properties, mark what is optional, use readonly for fields that should not change, and nest types to match the data hierarchy. The result is code that is self-documenting and safe from common data bugs.