Generics Deep Dive
🔶 Generics Deep Dive
Generic Constraints: Restricting Type Parameters
By default, a type parameter T can be anything. Constraints let you say "T must satisfy this requirement":
Key syntax: K extends keyof T means "K must be a key that exists on T."
Extends a Type
T extends string — T must be a string or a subtype of string.
Extends a Union
T extends string | number — T must be one of these types.
Extends keyof
K extends keyof T — K must be a property name of T.
Multiple Constraints
Chain them: <T extends { length: number }> — T must have a length property.
Example: Pick from an Array with Constraints
Default Type Parameters
Just like function parameters, type parameters can have defaults:
Variance: Covariance & Contravariance
Variance describes how generic types relate when their type parameters are subtypes of each other. This is subtle but crucial:
Covariance
If Dog extends Animal, then Box<Dog> extends Box<Animal>. Read-only types.
Contravariance
If Dog extends Animal, then Callback<Animal> extends Callback<Dog>. Function parameters.
Invariance
Box<Dog> is not assignable to Box<Animal>. Mutable types.
Why It Matters
Prevents type errors. Contravariance protects function parameter safety.
keyof and typeof: Type Introspection
keyof gets property names; typeof gets the type of a value:
Generic Utility Types: The Toolkit
TypeScript provides built-in generic utilities for common transformations:
Partial / Required
Toggle optionality. Partial<T> makes all optional; Required<T> makes all required.
Pick / Omit
Slice a type. Pick selects properties; Omit excludes them.
Record
Build a type from keys. Record<K, V> creates an object with keys K, values V.
Extract / Exclude
Filter unions. Extract keeps matching types; Exclude removes them.
Building Your Own Generic Utilities
Chain utilities and constraints to build powerful reusable patterns:
Example: Extracting Function Parameters
Recursive Generics: Types That Call Themselves
Generics can reference themselves for deeply nested structures:
🏗️ Real-World Pattern: Generic API Response Handler
Type-Safe Fetch Wrapper
💻 Coding Challenges
Challenge 1: Generic with Constraints
Write a function that takes an object and a key, and returns the value. Use constraints to ensure the key exists on the object. Bonus: make the return type exactly match the property type.
Goal: Practice keyof constraints and type inference.
Challenge 2: Build a Utility Type
Create a utility type GetType<T, K> that takes an object type and a key, and returns the type of that property. Test it on a sample interface.
Goal: Combine keyof, conditional types, and generics.
Challenge 3: Recursive Generic
Create a type Flatten<T> that "unwraps" nested arrays. For example, Flatten<[1, [2, 3]]> should be 1 | 2 | 3.
Goal: Build recursive type logic with array handling.
Constraints are powerful but can make code rigid. Before constraining a type parameter, ask: "Does this really need to be restricted?" Sometimes accepting any (though not ideal) is simpler than a complex constraint. Use constraints to enforce safety, not just to feel thorough.
🎯 What's Next
With generics mastered, we'll explore Decorators & Metadata — a powerful (and experimental) feature for attaching type information and behavior to classes and properties.