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
42: Generic Constraints
I remember a developer on my team a few years back who was trying to build a generic "Analytics Logger." The goal was simple: the logger should take any object, but it had to be an object that contained a userId property so we could track who was doing what. They started with a simple generic function: function logEvent<T>(event: T) { ... }.
The moment they tried to access event.userId inside the function, TypeScript threw a fit. It told them that userId didn't exist on type T. Of course it didn't! As far as TypeScript was concerned, T could be a string, a boolean, or a function. The developer's first instinct was to cast the event to any to make the error go away. I had to stop them right there. Casting to any is like turning off the lights because you can't find your keys; it solves the immediate frustration, but you're still in the dark. That's where generic constraints come in.
Narrowing the Scope of T
When you use a plain generic like <T>, you're telling TypeScript, "This can be literally anything." But usually, you don't actually want anything; you want something that fits a specific shape. We use the extends keyword to create a constraint. This tells TypeScript that while T can be any type, it must at least satisfy the requirements of a specific interface or type.
interface HasId {
id: string | number;
}
function printId<T extends HasId>(item: T): void {
// TypeScript now knows for a fact that 'id' exists on 'item'
console.log(`The ID is: ${item.id}`);
}
const user = { id: 1, name: "Alice" };
const product = { id: "prod_123", price: 29.99 };
printId(user); // Works great
printId(product); // Works great
printId({ name: "Bob" }); // Error: Property 'id' is missing
Notice that we aren't forcing item to be exactly a HasId object. We're saying it must extend it. This means the object can have ten other properties (like name or price), and TypeScript will still preserve those types for the rest of the function's logic while guaranteeing that id is present.
Combining Multiple Constraints
Sometimes, a single interface isn't enough. You might need a generic that satisfies two or three different requirements. You can't list multiple extends keywords one after another, but you can use an intersection type (the & operator) to combine them. I use this frequently when building plugins or middleware where an object needs to be both "configurable" and "executable."
interface Loggable {
log(): void;
}
interface Serializable {
toJSON(): string;
}
// T must be BOTH Loggable AND Serializable
function processData<T extends Loggable & Serializable>(item: T) {
item.log();
const json = item.toJSON();
console.log(`Processed: ${json}`);
}
const myService = {
log: () => console.log("Logging..."),
toJSON: () => JSON.stringify({ status: "ok" }),
version: "1.0.0"
};
processData(myService); // Valid!
By using constraints, you get the best of both worlds: the flexibility of generics and the safety of strong typing. You stop guessing what T is and start defining exactly what T is required to be.
📋 Practical Task
Build a Generic "Length-Aware" Utility
In this exercise, you need to create a utility function that can return the length of a value. However, not everything in JavaScript has a .length property (for example, numbers or booleans do not).
Your Task:
- Define an interface called
HasLengththat specifies alengthproperty as anumber. - Write a generic function called
getLength<T extends HasLength>that takes an argument of typeTand returns itslength. - Test the function with a string, an array of numbers, and an object like
{ length: 10, name: "Custom Object" }. - Verify that passing a
numberor abooleanto the function results in a TypeScript compilation error.
There are no comments for now.