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
109: Constraining Generics with keyof
A few years ago, I was reviewing a PR from a developer who was building a dynamic settings dashboard. He had written a utility function to pull specific configuration values out of a large settings object. Initially, his function looked something like this: function getConfigValue<T>(settings: T, key: string) { return settings[key]; }. He was baffled when TypeScript started screaming at him, claiming that an index signature was missing. Even worse, when he forced it to work using any, he realized he had no autocomplete for the keys, and a single typo in a string literal was causing silent runtime failures.
The problem was that he had told TypeScript that settings was some type T, but he told it that key was any string in the universe. TypeScript couldn't guarantee that the string passed as the key actually existed on the object. To fix this, we need to create a formal relationship between the object's type and the key's type.
Linking Keys to Types with keyof
The keyof operator is a powerful tool that takes an object type and produces a union of its keys. If you have a User interface with id, email, and username, keyof User literally evaluates to "id" | "email" | "username". It's like telling TypeScript, "I only care about the labels on this specific box."
When we combine this with generics, we can use a "constraint." Instead of just using T, we introduce a second generic, K, and tell TypeScript that K must be a subset of the keys of T. Here is how that looks in practice:
interface AppConfig {
apiUrl: string;
timeout: number;
retries: number;
}
function getSetting<T, K extends keyof T>(config: T, key: K) {
return config[key];
}
const myConfig: AppConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
};
// This works perfectly and provides autocomplete for 'apiUrl', 'timeout', etc.
const url = getSetting(myConfig, "apiUrl");
// This will trigger a compile-time error because "version" isn't a key of AppConfig
const version = getSetting(myConfig, "version");
The Magic of Type Inference
One of the best parts about this pattern is that you rarely have to explicitly pass the types when calling the function. Because you're passing myConfig as the first argument, TypeScript infers that T is AppConfig. Once T is locked in, it knows that K must be one of the keys of AppConfig. This creates a tight loop of type safety where the second argument is constrained by the first.
I often see developers try to avoid this by using Record<string, any>, but that's essentially giving up. By using K extends keyof T, you aren't just stopping errors; you're documenting the code. Anyone using your function will see exactly which keys are valid just by hovering over the argument in their IDE.
Handling Value Types
You'll notice that the return type of the function above is automatically inferred as the specific type of the property you accessed. If you pass "timeout", the return type is number; if you pass "apiUrl", it's string. This is the real power of constraining generics. We aren't just validating the input; we are preserving the type information all the way through to the output, avoiding the dreaded any cast that plagues so many large-scale TypeScript projects.
📋 Practical Task
Exercise: Building a Type-Safe Event Emitter Lookup
You are building a small Event Emitter system. You have an Events interface that defines the payload for different event types. Your goal is to create a function called getEventPayload that takes the Events object and a specific event name, returning the correctly typed payload.
Complete the code below by adding the necessary generic constraints to the getEventPayload function so that it only accepts valid event names and returns the correct value type.
interface Events {
userLoggedIn: { userId: string; timestamp: number };
orderPlaced: { orderId: string; amount: number };
connectionLost: { reason: string };
}
const eventStore: Events = {
userLoggedIn: { userId: "u123", timestamp: 1625097600 },
orderPlaced: { orderId: "o456", amount: 99.99 },
connectionLost: { reason: "Timeout" },
};
// TODO: Fix the generic signature of this function
function getEventPayload<T, K extends keyof T>(events: T, eventName: K) {
return events[eventName];
}
// Test cases:
const loginData = getEventPayload(eventStore, "userLoggedIn"); // Should be inferred as { userId: string; timestamp: number }
const orderData = getEventPayload(eventStore, "orderPlaced"); // Should be inferred as { orderId: string; amount: number }
// This should throw a TypeScript error:
// const invalidData = getEventPayload(eventStore, "somethingRandom");
There are no comments for now.