-
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
207: Practice Exercise: Building a Type-Safe Path Getter (Deep Get)
We've all been there: you're dealing with a massive API response or a deeply nested configuration object, and you find yourself writing if (user && user.profile && user.profile.address) just to get to a zip code. It's tedious. Naturally, the first instinct is to write a helper function—a "deep get"—that takes a path string like "profile.address.zip" and does the digging for you.
The "Just Trust Me" Approach
If you're in a rush, you might write something like this. It's clean, it's short, and it works at runtime:
function getDeepValue(obj: any, path: string) {
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
const data = { user: { settings: { theme: 'dark' } } };
const theme = getDeepValue(data, 'user.settings.theme'); // returns 'dark'
From a JavaScript perspective, this is fine. But from a TypeScript perspective, we've just committed a crime. Look at the return type of theme: it's any. We've essentially told the compiler, "Stop helping me; I'll handle it." The moment you use theme elsewhere, you lose all autocomplete and type safety. Even worse, if you typo the path as 'user.setttings.theme', TypeScript won't blink. You'll just get undefined at runtime, and you'll spend twenty minutes debugging a typo that the compiler should have caught in milliseconds.
The Cost of the Lie
The problem here is a disconnect between the path string and the actual structure of the obj. To the compiler, "user.settings.theme" is just a string. It has no inherent relationship to the keys of your object. To fix this, we need to move away from string and toward Template Literal Types. We want a type that says: "This string must be a valid key of the object, or a valid key of the object followed by a dot and then a valid key of the nested object."
Mapping the Path with Recursive Types
This is where things get interesting. We can use a recursive conditional type to map the path string to the actual value it points to. I usually approach this by thinking about the path in two parts: the first key (the "head") and the rest of the path (the "tail").
type PathValue =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
function deepGet<T, P extends string>(obj: T, path: P): PathValue<T, P> {
return path.split('.').reduce((acc, key) => acc?.[key], obj) as any;
}
Let's break down what's happening in PathValue. We use infer Key to grab the part of the string before the first dot. If that Key exists on T, we recursively call PathValue using the value at that key and the Rest of the string. If we hit a string with no dots left, we check if that final segment is a key of the current object level. If it is, we return the type of that value. If at any point the path diverges from the object structure, it collapses to never.
Where the Complexity Hits the Fan
Now, I'll be honest with you: this isn't a free lunch. While this gives us incredible IDE support—you'll actually get autocomplete for the path strings now—it adds a layer of cognitive load. Recursive types can be a bit of a brain-bender when you come back to them six months later. There's also a performance cost; if you have an object that is ten levels deep and a path string that is equally long, the TypeScript compiler has to do a lot of work to resolve those types.
In a small project, this might be overkill. But if you're building a shared utility library or working on a mission-critical data layer, the trade-off is worth it. You're moving the "error" from the user's browser at runtime to the developer's editor at compile time. In my book, that's always a win.
📋 Practical Task
Exercise: Implementing a Type-Safe Deep-Get Utility for Configuration Objects
You are tasked with creating a type-safe configuration reader for a complex application settings object. The goal is to ensure that any attempt to access a configuration value using a path string is validated by TypeScript.
Requirements:
- Define a
Configinterface with at least three levels of nesting (e.g.,app.database.connection.timeout). - Implement the
PathValuerecursive type to resolve the type of a value based on a dot-notation string path. - Implement the
deepGetfunction using thePathValuetype for its return value. - Test your implementation by:
- Successfully retrieving a deeply nested value with full type inference.
- Intentionally passing an invalid path string to verify that TypeScript flags it as an error.
Starter Code:
interface Config {
api: {
endpoints: {
users: string;
posts: string;
};
timeout: number;
};
theme: {
colors: {
primary: string;
secondary: string;
};
darkMode: boolean;
};
}
// Your PathValue type and deepGet function here...
There are no comments for now.