-
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
74: Bridging Compile-Time Types and Runtime Validation
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
Productwith:id(number),name(string), andprice(number). - Write a type guard function
isValidProduct(data: any): data is Productthat 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)
There are no comments for now.