Skip to Content
Course content

234: Optional Type Parameters with Defaults

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 StoreState that represents a basic state object (a record of strings to unknowns).
  • Create a class called StateStore<T>.
  • Make the type parameter T optional, defaulting to StoreState.
  • Add a private property state: T and a method getState(): T that returns the current state.
  • Instantiate the store twice:
    1. Once without providing a type (relying on the default).
    2. Once providing a custom UserSettings interface (containing theme: string and notifications: boolean).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.