-
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
208: Practice Exercise: Building a JSON Schema to Type Mapper
You've probably run into this before: your application is driven by a configuration file or a JSON schema coming from a database, and you find yourself wishing TypeScript could just "see" the structure of that schema and give you autocomplete for the data it describes. It's a common gap. The schema says the data is a string, but as far as TypeScript is concerned, that data is just some opaque object.
The temptation of manual casting and any
When I first tackled this, I took the path of least resistance. I wrote a function that validated the JSON schema at runtime and then just returned the result as any or used a type assertion like as UserProfile. It felt fast. In the short term, it works because the code runs, and you've told the compiler to stop complaining.
// The "I'm in a hurry" approach
function parseSchema(schema: any, data: any) {
// Imagine some runtime validation logic here
return data as any;
}
const user = parseSchema(userSchema, rawData);
console.log(user.name.toUpperCase()); // No error here, but no autocomplete either.
The problem is that you've effectively turned off TypeScript. If the schema changes—say, name becomes an object with first and last—your type assertion remains any or stays stuck as the old UserProfile. You've traded a few minutes of setup for a lifetime of runtime TypeError: cannot read property 'toUpperCase' of undefined. You're essentially lying to your future self.
Leveraging recursive conditional types
The better way—and the way we'll implement today—is to treat the schema itself as a type. By using recursive conditional types, we can tell TypeScript: "Look at the type property of this schema. If it's 'string', the resulting value is a string. If it's 'object', look at the properties and repeat this logic for every key."
It looks a bit more intimidating at first, but it's incredibly powerful. We aren't casting; we're mapping.
type JsonSchema =
| { type: 'string' }
| { type: 'number' }
| { type: 'boolean' }
| { type: 'object'; properties: Record<string, JsonSchema> };
type SchemaToType<T> = T extends { type: 'string' }
? string
: T extends { type: 'number' }
? number
: T extends { type: 'boolean' }
? boolean
: T extends { type: 'object'; properties: infer P }
? { [K in keyof P]: SchemaToType<P[K]> }
: never;
// Now we define the schema as a 'const' so TS preserves the literal values
const userSchema = {
type: 'object',
properties: {
username: { type: 'string' },
age: { type: 'number' },
settings: {
type: 'object',
properties: {
notifications: { type: 'boolean' }
}
}
}
} as const;
type User = SchemaToType<typeof userSchema>;
// User is now: { username: string; age: number; settings: { notifications: boolean; } }
Notice the as const on the schema definition. This is the secret sauce. Without it, TypeScript sees type: 'string' as just a general string, and our conditional types won't be able to narrow it down to the specific literal 'string'.
The cost of type-level recursion
Is this approach always right? Not necessarily. You're trading simplicity for strictness. The SchemaToType utility is significantly harder for a junior dev to read than a simple type assertion. Also, if you have a truly massive, deeply nested schema, you can occasionally hit TypeScript's recursion limit, though that's rare for most real-world configs.
But in my experience, the trade-off is almost always worth it. When you change a field in your schema and suddenly five different files in your IDE turn red with errors, that's not a nuisance—that's the type system doing exactly what you hired it to do. It's preventing a production bug before you've even hit 'Save'.
📋 Practical Task
Exercise: Implementing a Recursive Schema Mapper for Product Catalogs
You are building a product management system where the structure of a "Product" is defined by a JSON schema. Your goal is to implement the type mapping logic so that the product data is fully typed based on the schema definition.
Requirements:
- Define a
JsonSchematype that supports'string','number', and'object'types. - Implement a
SchemaToType<T>conditional type that recursively maps these schema definitions to actual TypeScript types. - Create a
productSchemaobject (usingas const) that describes a product with:title(string)price(number)metadata(an object containingskuas a string andweightas a number)
- Create a type
Productusing your mapper and a variablemyProductof that type to verify that the nestedmetadataproperties are correctly typed.
There are no comments for now.