-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
14: Practice Exercise: Typing a Simple Data Model
By now, you know how to declare types, but there's a massive difference between knowing the syntax and actually designing a data model that doesn't collapse the moment your project grows. I've seen a lot of developers treat TypeScript like a suggestion—something they fight against until the red squiggly lines go away—rather than using it as a blueprint for their application.
Let's look at a real scenario: we're building a small system to track a library's collection. You've got books, authors, and categories. It seems simple enough that you might be tempted to just wing it.
The "I just want it to compile" approach
When I'm reviewing code from juniors, I often see a pattern where they use any or a very generic object type because they're in a rush. It looks something like this:
const books = [
{ title: "The Hobbit", author: "Tolkien", pages: 310, available: true },
{ title: "1984", author: "Orwell", pages: 328, available: false }
];
function checkoutBook(book: any) {
book.available = false;
console.log(`You checked out ${book.titel}`);
}
On the surface, this works. The code runs, and the logic is there. But notice the bug I snuck into that console.log? I wrote book.titel instead of book.title. Because I used any, TypeScript just shrugged its shoulders and let me commit that bug to the codebase. In a real app, that's a runtime error that crashes the UI for the user, and you've completely bypassed the primary reason we're using TypeScript in the first place.
Investing in a formal schema
The better way is to treat your data models as a contract. Instead of letting the data be "whatever," we define exactly what a Book is. I prefer using interfaces for these kinds of structures because they're clean and extensible.
type BookStatus = 'available' | 'checked-out' | 'lost';
interface Author {
name: string;
nationality: string;
}
interface Book {
isbn: string;
title: string;
author: Author;
pages: number;
status: BookStatus;
}
const library: Book[] = [
{
isbn: "978-0261102217",
title: "The Hobbit",
author: { name: "J.R.R. Tolkien", nationality: "British" },
pages: 310,
status: 'available'
}
];
Now, look at what we've gained. First, we used a Union Type for BookStatus. If I try to set the status to "borrowed", TypeScript will stop me because "borrowed" isn't in our allowed list. We've moved the error from the user's browser to our editor.
Second, we nested our models. By making author its own Author interface, we can reuse that type elsewhere—maybe for an author biography page—without duplicating the definition. If we ever need to add a birthYear to the author, we do it in one place, and every book in the system is updated automatically.
The trade-off: Up-front cost vs. long-term sanity
I'll be honest with you: the second approach takes more keystrokes. It feels slower at first. You have to stop and think about the shape of your data before you write the logic. But here is the trade-off: you are trading a few minutes of design time for hours of debugging time later.
When you use a strict data model, your IDE becomes your best friend. The moment you type book., you get a perfect list of every property available. You don't have to jump back and forth between files to remember if you called the property authorName or just author. You're essentially documenting your code as you write it, and that documentation is guaranteed to be accurate because if it weren't, the code wouldn't compile.
📋 Practical Task
Exercise: Modeling a Project Management Dashboard
You are building a task tracker. Currently, the project uses a very loose structure that is causing bugs. Your goal is to refactor the data model to be type-safe.
Requirements:
- Create a union type called
Prioritythat only allows 'Low', 'Medium', or 'High'. - Create an interface called
Userwithid(number) andusername(string). - Create an interface called
Taskthat includes:id(string)title(string)assignedTo(must use theUserinterface)priority(must use thePriorityunion type)isCompleted(boolean)
- Create an array of
Taskobjects and attempt to add a task with an invalid priority to see the TypeScript compiler catch the error.
There are no comments for now.