TypeScript
Completed
-
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
73: Runtime Validation with io-ts
One of the biggest lies we tell ourselves in TypeScript is that a type cast—like as User—actually does something. It doesn't. It just tells the compiler to stop complaining. If your API sends back a null where you expected a string, your app will still crash at runtime, but the compiler will be blissfully unaware until the Cannot read property 'x' of null error hits your logs.
This is where io-ts comes in. It allows us to define a "codec"—a single source of truth that acts as both a runtime validator and a TypeScript type definition. I've used this in several production projects where we deal with unstable third-party APIs, and it's a lifesaver.
Defining our data contract with codecs
Let's say we're building a small dashboard that fetches a user's GitHub profile. We can't trust the API response implicitly. Instead of a standard interface, we'll use io-ts to define what a "User" looks like. I'm going to use the t namespace from io-ts to build this.
import * as t from 'io-ts';
import { isLeft } from 'fp-ts/Either';
const UserCodec = t.type({
id: t.number,
login: t.string,
bio: t.string,
public_repos: t.number,
});
Notice that UserCodec isn't a type; it's a value. It's an object that knows how to check if a random piece of data matches the structure we defined. This is the "runtime" part of the equation.
The mistake: Confusing the codec with the type
When I first started with io-ts, I made a classic mistake. I tried to use the codec itself as the type for my variables. I wrote something like this:
// ❌ THIS IS WRONG
const profile: UserCodec = {
id: 1,
login: 'octocat',
bio: 'GitHub mascot',
public_repos: 10
};
TypeScript immediately screamed at me. Why? Because UserCodec is the validator, not the shape of the data. The validator is a complex object with methods like decode and encode. It's not a user profile.
To fix this, io-ts provides a helper called t.TypeOf. This extracts the actual TypeScript interface from the codec so we don't have to write the interface twice (once for runtime and once for compile-time).
// ✅ This is the way
type User = t.TypeOf<typeof UserCodec>;
const profile: User = {
id: 1,
login: 'octocat',
bio: 'GitHub mascot',
public_repos: 10
};
Handling the "Either" result
Now for the actual validation. When we call UserCodec.decode(data), io-ts doesn't throw an error if the validation fails. Instead, it returns an Either type from the fp-ts library. An Either is basically a fancy way of saying "this is either a Left (an error) or a Right (the successfully validated data)."
Here is how I typically implement a fetcher using this pattern:
async function fetchUser(username: string): Promise<User | null> {
const response = await fetch(`https://api.github.com/users/${username}`);
const json = await response.json();
const result = UserCodec.decode(json);
if (isLeft(result)) {
console.error('API response did not match the UserCodec:', result.left);
return null;
}
// result.right is now fully typed as User
return result.right;
}
I prefer this approach because it forces me to handle the failure case explicitly. If the API changes a field name from login to username, my app won't crash with a weird undefined error deep in the UI components; it will fail right at the edge of the system where the data enters the app.
📋 Practical Task
Exercise: Building a Product Catalog Validator
You are integrating with an e-commerce API that returns a list of products. The API is notoriously flaky and sometimes sends numbers as strings or omits required fields.
Your Task:
- Install
io-tsandfp-ts(conceptually, or in your local environment). - Create a
ProductCodecthat validates the following structure:sku: A stringprice: A numbertags: An array of strings
- Define a TypeScript type
Productusingt.TypeOfbased on your codec. - Write a function
validateProduct(data: unknown): Product | string.- If the data is valid, return the validated
Product. - If the data is invalid, return the string
"Invalid Product Data".
- If the data is valid, return the validated
- Test your function with these two objects:
{ sku: "TS-101", price: 29.99, tags: ["book", "typescript"] }(Should succeed){ sku: "TS-101", price: "29.99", tags: ["book"] }(Should fail because price is a string)
There are no comments for now.