Skip to Content
Course content

14: Practice Exercise: Typing a Simple Data Model

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Priority that only allows 'Low', 'Medium', or 'High'.
  • Create an interface called User with id (number) and username (string).
  • Create an interface called Task that includes:
    • id (string)
    • title (string)
    • assignedTo (must use the User interface)
    • priority (must use the Priority union type)
    • isCompleted (boolean)
  • Create an array of Task objects and attempt to add a task with an invalid priority to see the TypeScript compiler catch the error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.