Skip to Content
Course content

74: Bridging Compile-Time Types and Runtime Validation

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

Imagine you're a customs officer at an international airport. A traveler hands you a passport that says they are a citizen of Canada. In a perfect world, you just look at the passport and let them through. But in the real world, passports can be forged. If you just trust the document without checking the person's face or verifying the security holographic seal, you're letting a potential security risk into the country.

In TypeScript, your interfaces and types are that passport. They tell the compiler, "Trust me, this object is a User." But here is the catch: once your code is compiled to JavaScript and running in the browser, those types are completely erased. They don't exist. If an API sends back a null where you expected a string, TypeScript won't stop the app from crashing at runtime because the "passport" was just a piece of paper that disappeared during the flight.

The Great TypeScript Lie

I see this happen constantly in professional codebases. A developer fetches data from a REST API and does something like this:

const user = await response.json() as UserProfile;

That as UserProfile is what I call "The Lie." You aren't actually converting the data; you're just telling TypeScript to stop complaining and trust you. If the API changes—maybe user.email becomes user.contact_email—your code will compile perfectly, but your app will blow up the second a user hits that page.

Building a Digital Bouncer

To fix this, we need a "bouncer"—a function that actually checks the data at runtime and tells TypeScript, "Yes, I've verified this, it's safe to use as this type." We do this using Type Predicates.

Let's look at a real example. Suppose we're consuming a Weather API that returns a forecast. We want to ensure the data has a temperature and a unit before we try to render it.

interface WeatherData {
  temperature: number;
  unit: 'C' | 'F';
  city: string;
}

// This is our "Bouncer" function
function isWeatherData(data: any): data is WeatherData {
  return (
    typeof data.temperature === 'number' &&
    (data.unit === 'C' || data.unit === 'F') &&
    typeof data.city === 'string'
  );
}

Notice the return type: data is WeatherData. This isn't just a boolean; it's a signal to the TypeScript compiler. If this function returns true, TypeScript will treat the variable as WeatherData for the rest of that block.

Connecting the Dots in Practice

Now, instead of using a dangerous type cast, we wrap our API call in a validation check. This bridges the gap between the "unknown" world of the network and the "typed" world of your application logic.

async function getWeather(city: string) {
  const response = await fetch(`/api/weather?q=${city}`);
  const rawData = await response.json();

  if (isWeatherData(rawData)) {
    // Inside this block, TypeScript knows exactly what rawData is.
    // You get full autocomplete for .temperature, .unit, and .city.
    console.log(`It is ${rawData.temperature}${rawData.unit} in ${rawData.city}`);
  } else {
    // Handle the error gracefully instead of crashing the UI
    console.error("API returned malformed weather data!");
    throw new Error("Invalid server response");
  }
}

I'll be honest: writing these guards by hand for huge objects is a nightmare. In larger projects, I usually reach for libraries like Zod or Runtypes. They let you define a schema once and automatically generate both the TypeScript type and the runtime validator. But whether you use a library or a manual guard, the philosophy is the same: never trust data that comes from outside your own code.




📋 Practical Task

Implementing a Secure Product API Validator

You are building an e-commerce dashboard. The backend sends a Product object, but the API is notoriously unstable and sometimes misses fields or sends the wrong types.

Your Goal: Create a type predicate function to validate a Product object and use it to safely process an API response.

Requirements:

  • Define an interface Product with: id (number), name (string), and price (number).
  • Write a type guard function isValidProduct(data: any): data is Product that verifies all three fields exist and have the correct types.
  • Create a function processProductResponse(json: any) that uses your guard. If valid, it should return a string: "Product [name] costs $[price]". If invalid, it should return "Invalid product data received".
// Test your implementation with these cases:
const case1 = { id: 101, name: "Mechanical Keyboard", price: 120 }; // Should be valid
const case2 = { id: 102, name: "Gaming Mouse", price: "45" };      // Should be invalid (price is string)
const case3 = { id: 103, price: 20 };                             // Should be invalid (missing name)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.