Skip to Content
Course content

125: Writing Types That Match Real Runtime Shapes

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

Imagine you've ordered a meal kit delivery. Inside the box is a recipe card—that's your documentation. The card says, "This kit contains two carrots, one onion, and a bunch of parsley." You don't even look in the box; you just trust the card. You reach in, grab what you assume is a carrot, and start chopping. But when you look down, you realize the company sent you a potato instead. Or worse, the box is empty.

In TypeScript, we do this all the time with API calls. We define a beautiful interface for a User profile, and then we tell TypeScript: "Trust me, this JSON coming back from the server is a User." We're essentially chopping the potato while thinking it's a carrot. The code compiles perfectly, but the app crashes the moment a user hits that page because user.address.street is trying to read a property of undefined.

The Danger of the "Trust Me" Cast

I've seen this happen in countless production environments. It usually looks like this:

interface UserProfile {
  id: string;
  email: string;
  preferences: {
    theme: 'light' | 'dark';
  };
}

async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // This is the "Trust Me" cast. It's a lie.
  return data as UserProfile; 
}

When you use as UserProfile, you aren't actually changing the data; you're just telling the compiler to stop asking questions. If the API changes and preferences becomes null, TypeScript won't warn you. You've effectively turned off the safety switch for that entire object. I generally consider as to be a "code smell" when dealing with external data.

Closing the Gap with Runtime Validation

To write types that actually match reality, we have to move our checks from the compile-time (where TypeScript lives) to the runtime (where the JavaScript actually executes). We need to stop assuming and start verifying.

The most robust way to do this is through a "Type Guard" or a validation schema. Instead of lying to the compiler, we write a function that proves the data is what we think it is. If it isn't, we handle the error gracefully instead of letting the app explode.

function isUserProfile(data: any): data is UserProfile {
  return (
    typeof data.id === 'string' &&
    typeof data.email === 'string' &&
    data.preferences && 
    typeof data.preferences.theme === 'string'
  );
}

async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  if (!isUserProfile(data)) {
    throw new Error("API returned a malformed user profile!");
  }

  // Now, TypeScript knows for a FACT that data is a UserProfile
  return data; 
}

Designing for the "Messy" Truth

Here is a bit of a hard truth: APIs are rarely as clean as our interfaces. Sometimes a field is missing, sometimes it's null, and sometimes it's a string when it should be a number. If you find yourself writing a Type Guard and realizing that 50% of your fields are optional, your interface is lying to you.

I recommend being "pessimistic" with your types. If a field isn't guaranteed 100% of the time by the backend team, mark it as optional (?) or allow null. It's much better to be forced by the compiler to handle a null case now than to debug a "Cannot read property of null" error in your logs at 2 AM.




📋 Practical Task

Hardening the Legacy Weather API Response

You are integrating with an old weather API. The documentation claims it returns a WeatherReport object, but in reality, the temperature is sometimes missing, and the windSpeed is occasionally returned as a string instead of a number.

Your Task: Implement a Type Guard function called isValidWeatherReport that ensures the runtime data matches the WeatherReport interface. Then, use that guard inside the getWeather function to prevent the app from processing invalid data.

interface WeatherReport {
  city: string;
  temperature: number;
  windSpeed: number;
}

async function getWeather(city: string) {
  // Simulating a messy API response
  const responses = [
    { city: "New York", temperature: 72, windSpeed: 10 }, // Valid
    { city: "London", windSpeed: 15 },                    // Missing temperature
    { city: "Tokyo", temperature: 65, windSpeed: "high" }, // windSpeed is a string
  ];
  
  const data = responses[Math.floor(Math.random() * responses.length)];

  // TODO: Implement the type guard check here.
  // If the data is invalid, log "Invalid data received" and return null.
  // If valid, return the data.
}

// TODO: Implement this Type Guard
function isValidWeatherReport(data: any): data is WeatherReport {
  // Your logic here
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.