Q21 of 38 · TypeScript
What is the difference between `extends` in class inheritance versus `extends` in a generic constraint?
Short answer
Short answer: `class Dog extends Animal` is runtime inheritance — `Dog` inherits Animal's methods and prototype. `function fn<T extends Serializable>` is a compile-time generic constraint — `T` must be assignable to `Serializable`, but no inheritance occurs. The keyword is the same; the mechanism is entirely different.
Detail
TypeScript overloads the extends keyword for two distinct purposes that are often confused.
Class extends (runtime): class Dog extends Animal sets up the JavaScript prototype chain. Dog.prototype.__proto__ === Animal.prototype. The constructor calls super() to initialize the parent. This is a runtime mechanism that produces actual JavaScript code.
Generic constraint (compile-time): function serialize<T extends Serializable>(value: T) is purely a type-level restriction. It says "T must be assignable to Serializable". No inheritance occurs — there is no runtime impact. The constraint is erased from the compiled JavaScript.
Conditional type extends: T extends U ? X : Y is a third use — a type-level conditional check. Also compile-time only.
Interface extends: interface AdminUser extends User merges the type declarations — AdminUser has all User properties plus its own. This is structural type merging, not prototype inheritance.
In test automation: Generic constraints appear constantly in Playwright's type system — test.extend<T extends {}>(...). Understanding the compile-time vs runtime distinction prevents confusion when reading type errors from generically constrained functions.
// EXAMPLE
// Class extends — runtime prototype chain
class BasePage {
navigate(url: string) { return; }
}
class LoginPage extends BasePage {
fillCredentials(u: string, p: string) { return; }
}
const page = new LoginPage();
page.navigate("/login"); // inherited method — runtime
// Generic extends — compile-time constraint only
interface HasId { id: number; }
function getById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// T must have an id property — no class hierarchy needed
// Interface extends — type merging
interface User { name: string; }
interface AdminUser extends User { adminLevel: number; }
// AdminUser = { name: string; adminLevel: number }
// All three 'extends' in one snippet — compile-time vs runtime
class Repository<T extends HasId> extends BasePage { // both!
findById(id: number) { /* ... */ }
}