T extends { timeout: number } means "some type that at least has timeout", not "exactly { timeout: number }". Returning a hand-built default as T is therefore refused (TS2322: "'T' could be instantiated with a different subtype of constraint"). The usual reaction is to silence it with as T, and that is where the real bug starts: a caller asks for a bigger T and receives an object missing the extra fields.
function withDefaults<T extends { timeout: number }>(opts?: T): T {
return opts ?? ({ timeout: 1000 } as T); // silencing the error...
}
const settings = withDefaults<{ timeout: number; retries: number }>();
console.log(settings.retries); // ...and the caller is lied to
// undefined (typed as number)Without a constraint, a generic type parameter T could be absolutely anything, so TypeScript only allows operations valid for every conceivable type — which in practice means almost nothing. Writing T extends SomeType restricts T to types assignable to SomeType, letting the function safely use SomeType's members on any value of type T. A common and useful constraint is keyof another generic, restricting one parameter's valid values to the property names of another.
The problem an unconstrained generic has
TypeScript has no idea T even has a .length property — T could be a number, a boolean, anything.
function logLength<T>(item: T) {
// console.log(item.length); // COMPILE ERROR: T might not have a length property
}Adding a constraint with extends
Now T is restricted to any type with a .length property, so accessing it is safe — while T can still be a string, an array, or any custom type that happens to have that shape.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T) {
console.log(item.length); // safe: T is guaranteed to have .length
}
logLength("hello"); // fine — string has .length
logLength([1, 2, 3]); // fine — array has .length
// logLength(42); // COMPILE ERROR: number doesn't have .lengthkeyof constraints: restricting to another type's keys
This is exactly how TypeScript's built-in Pick<T, K> and similar utility types constrain their key parameter — preventing a caller from passing a property name that doesn't actually exist on the object.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // fine, returns string
// getProperty(user, "email"); // COMPILE ERROR: "email" is not a key of userThe compiler's original error was correct: the function promises whatever T the caller chooses, and no fixed default can be every possible subtype. If the function really works with one known shape, it shouldn't be generic. Take a Partial of that shape and fill in every field from complete defaults:
type Options = { timeout: number; retries: number };
const defaults: Options = { timeout: 1000, retries: 3 };
function withDefaults(opts: Partial<Options> = {}): Options {
return { ...defaults, ...opts };
}
const settings = withDefaults();
console.log(settings.retries); // 3
console.log(withDefaults({ timeout: 50 })); // { timeout: 50, retries: 3 }