-
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
125: Writing Types That Match Real Runtime Shapes
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
}There are no comments for now.