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.
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.
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.
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:
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.
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:
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:
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:
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
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.
Frequently Asked Questions
Should I use an interface or a type alias for data models?
How do I handle data that might be missing or incomplete?
Can I nest interfaces to model complex data?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.