Q21 of 38 · TypeScript

What is the difference between `extends` in class inheritance versus `extends` in a generic constraint?

TypeScriptMidtypescriptextendsgenericsclassinheritanceconstraints

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) { /* ... */ }
}

// WHAT INTERVIEWERS LOOK FOR

Clear articulation that class extends is runtime (prototype chain) while generic extends is compile-time (constraint). Interface extends as type merging. Confusion between these three is very common — clarity here shows solid TypeScript grounding.

// COMMON PITFALL

Assuming generic constraints imply inheritance — `function fn<T extends Animal>` does NOT mean T must be a subclass of Animal at runtime. It means T must be structurally compatible with Animal (duck typing).