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
234: Optional Type Parameters with Defaults
I've seen this happen a lot when developers start building wrapper classes or API response handlers. You want the flexibility of generics, but you find yourself fighting the compiler because you're forced to be explicit even when the "default" case is obvious.
The "Missing Type Argument" Headache
Imagine you're building a standard wrapper for all your API responses. You want to be able to specify exactly what the data property contains, but for 80% of your endpoints, it's just a simple key-value object.
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// This works great for specific types
interface User { name: string; }
const userResponse: ApiResponse<User> = {
data: { name: "Alice" },
status: 200,
message: "Success"
};
// But here's where it breaks. I just want a generic object here.
const genericResponse: ApiResponse = {
data: { foo: "bar" },
status: 200,
message: "Success"
};
If you try to run this, TypeScript will throw an error: Generic type 'ApiResponse<T>' requires 1 type argument(s).
Now, you could "fix" this by writing ApiResponse<any> or ApiResponse<Record<string, unknown>> every single time. But that's tedious. It clutters the code and feels like you're fighting the tool rather than using it. I hate writing the same boilerplate over and over again, and you probably do too.
Setting a Sensible Default
The fix is surprisingly similar to how we handle default parameters in JavaScript functions. You can assign a default type to your generic parameter using the = operator. This makes the type parameter optional.
// By adding "= Record<string, unknown>", we tell TS:
// "If the developer doesn't provide a type, assume it's this."
interface ApiResponse<T = Record<string, unknown>> {
data: T;
status: number;
message: string;
}
// Now this works perfectly!
// TypeScript infers 'genericResponse' as ApiResponse<Record<string, unknown>>
const genericResponse: ApiResponse = {
data: { foo: "bar" },
status: 200,
message: "Success"
};
// And we still have the power to be specific when we need to be.
const userResponse: ApiResponse<User> = {
data: { name: "Alice" },
status: 200,
message: "Success"
};
The magic here is that the generic is now optional. When you omit the angle brackets, TypeScript doesn't panic; it just falls back to your default.
I usually recommend avoiding any as a default. Using Record<string, unknown> or a specific base interface is much safer because it forces you to perform type checking or casting when you actually access the data, preventing those "cannot read property of undefined" crashes in production.
📋 Practical Task
Exercise: Implementing a Flexible Data Store with Default State
You are building a StateStore class that manages the state of a UI component. Most components use a simple object for state, but some require complex, nested interfaces.
Your task:
- Create an interface called
StoreStatethat represents a basic state object (a record of strings to unknowns). - Create a class called
StateStore<T>. - Make the type parameter
Toptional, defaulting toStoreState. - Add a private property
state: Tand a methodgetState(): Tthat returns the current state. - Instantiate the store twice:
- Once without providing a type (relying on the default).
- Once providing a custom
UserSettingsinterface (containingtheme: stringandnotifications: boolean).
There are no comments for now.