Class Patterns
Classes beyond the basics — implements versus extends, why two same-shape classes are assignable, the polymorphic this type, mixins, method overriding under structural typing, and fluent builders that encode call order in their types.
10 questions
JuniorTheoryVery commonHow does implementing an interface differ from extending a class?
How does implementing an interface differ from extending a class?
extends inherits implementation — the base's members and its prototype chain — and a class has exactly one base. implements inherits nothing: it only checks at compile time that the class declares the interface's members, emits no code, and may be repeated for any number of interfaces.
Common mistakes
- ✗Expecting
implementsto inherit or auto-generate the interface's members - ✗Thinking a class can extend more than one base class
- ✗Believing an interface survives to runtime, so
instanceofcan test it
Follow-up questions
- →How would you share implementation between classes that cannot share a base?
- →Does adding an
implementsclause change the emitted JavaScript in any way?
JuniorTheoryVery commonWhy is an instance of one class assignable to another class type of the same shape?
Why is an instance of one class assignable to another class type of the same shape?
Because TypeScript compares types by structure, not by name: if Point and Vec2 declare the same public members, each is assignable to the other though neither extends the other. A class type has no nominal identity of its own; only a runtime instanceof tells them apart.
Common mistakes
- ✗Assuming a class name gives its type a nominal identity the checker respects
- ✗Thinking two classes are compatible only through a shared base or interface
- ✗Expecting the assignment to fail at runtime because the constructors differ
Follow-up questions
- →What single member declaration would make one of the two classes incompatible with the other?
- →Does a plain object literal with the same members satisfy a class type as well?
JuniorTheoryCommonWhat does a method whose declared return type is this mean in a class?
What does a method whose declared return type is this mean in a class?
this as a return type is the polymorphic this type: it stands for the type of the actual receiver, so an inherited method returns the subclass. That keeps a chain alive — new Child().reset().childOnly() compiles, while a Base return type would lose childOnly.
Common mistakes
- ✗Reading a
thisreturn type as the runtimethisbinding rather than a type - ✗Expecting an inherited method typed
thisto return the base class - ✗Declaring the return type as the class name, which drops the subclass from the chain
Follow-up questions
- →How does a
thisreturn type behave in an interface that several classes implement? - →What is the difference between a
thisreturn type and athisparameter?
MiddleCodeCommonKeep a fluent builder's chain typed across a subclass
Keep a fluent builder's chain typed across a subclass
Declare the return type as this, not the class name: where(cond: string): this, returning this. As the polymorphic this type it denotes the receiver's own class, so the subclass's limit() stays reachable in the chain; naming QueryBuilder there would drop it at the first link.
Common mistakes
- ✗Annotating the return type as the class name, which erases the subclass mid-chain
- ✗Re-declaring every chainable method in each subclass to restore its return type
- ✗Reaching for a self-referential generic base when
thisalready expresses it
Follow-up questions
- →What happens to the chain if one method returns
new QueryBuilder()instead ofthis? - →How would you make
build()unavailable untilwhere()has been called at least once?
MiddleTheoryCommonWhat does TypeScript actually check when a subclass overrides a base method?
What does TypeScript actually check when a subclass overrides a base method?
Only that the override stays assignable to the base member: the return type must be covariant, while method parameters are compared bivariantly, so narrowing one compiles though it is unsound. Nothing links the names either — a misspelled override adds a new member, which noImplicitOverride catches.
Common mistakes
- ✗Assuming an override's signature must match the base member exactly
- ✗Trusting a narrowed parameter override — method bivariance lets it through unsoundly
- ✗Expecting a misspelled override to be an error without
noImplicitOverride
Follow-up questions
- →Why does declaring the member as a property with a function type change the variance check?
- →What breaks at runtime when a subclass narrows a parameter that the base calls with a wider value?
MiddleCodeCommonLayer reusable behavior onto a class without multiple inheritance
Layer reusable behavior onto a class without multiple inheritance
A mixin is a function that takes a constructor type and returns a class expression extending it: function Stamped<T extends Ctor>(B: T) { return class extends B { at = new Date() }; }. The constructor constraint forwards the base's arguments, and because the result is itself a class, mixin calls nest.
Common mistakes
- ✗Copying methods onto a prototype and expecting the type to gain them
- ✗Constraining the base as a plain object type instead of a constructor type
- ✗Dropping the base's constructor arguments instead of forwarding
...args
Follow-up questions
- →How would you require the base to already have a member, e.g.
id: string, before the mixin applies? - →Why does giving the mixin an explicit return type usually make composition worse?
SeniorTheoryOccasionalWhen does a class stop being interchangeable with a same-shape interface?
When does a class stop being interchangeable with a same-shape interface?
A class type is compared structurally: an object literal or an unrelated class with the same public members satisfies it. A private or protected member breaks that — such members match only by declaration site — so two classes each declaring private id are mutually unassignable, and no literal satisfies either.
Common mistakes
- ✗Assuming a class type only accepts instances built by its own constructor
- ✗Expecting two classes with identical
privatemembers to be assignable to each other - ✗Thinking
privateis erased and therefore irrelevant to assignability
Follow-up questions
- →How does a subclass of a class with a
privatemember stay assignable to it? - →What is the difference in this respect between TypeScript's
privateand a JavaScript#field?
SeniorDesignOccasionalYou are designing a chainable HTTP client. The rules are: the URL must be set before a method is chosen; body() is legal after post() or put() but must be impossible after get(); send() is only legal once a URL and a method are both present; and header() may be called at any point, any number of times, without changing what is allowed next. Today all of this is enforced by runtime assertions inside send(). Describe how you would type the chain so that an invalid order — client.get(url).body({}) or a bare client.send() — is a compile error rather than a thrown exception, and say what a single runtime class has to do with the several types the chain moves through.
You are designing a chainable HTTP client. The rules are: the URL must be set before a method is chosen; body() is legal after post() or put() but must be impossible after get(); send() is only legal once a URL and a method are both present; and header() may be called at any point, any number of times, without changing what is allowed next. Today all of this is enforced by runtime assertions inside send(). Describe how you would type the chain so that an invalid order — client.get(url).body({}) or a bare client.send() — is a compile error rather than a thrown exception, and say what a single runtime class has to do with the several types the chain moves through.
Each call returns a different stage type, not the same class: url() gives a MethodStage with only the verbs, post() a BodyStage carrying body() and send(), get() a SendStage without body(). An illegal order is unrepresentable — the member is absent from the type you hold — and one runtime class implements every stage.
Common mistakes
- ✗Returning the same type from every call, so the type cannot remember where the chain is
- ✗Trying to narrow a state union by control flow across separate method calls
- ✗Assuming several stage types demand several runtime classes
Follow-up questions
- →How would you let
header()be callable from every stage without duplicating it in each interface? - →What is the trade-off between stage interfaces and one class generic over the set of calls made?
SeniorTheoryOccasionalWhy may an interface extend many interfaces while a class extends only one base?
Why may an interface extend many interfaces while a class extends only one base?
An interface only accumulates declarations: extending several merges their members, and a clash is an error at declaration — there is no implementation to linearize. A class carries implementation on one prototype chain, so a second base would make member lookup ambiguous. Many interfaces compose contracts, never code — reuse needs a mixin.
Common mistakes
- ✗Expecting
implementson several interfaces to supply any implementation - ✗Thinking the single-base rule is arbitrary rather than a prototype-chain constraint
- ✗Believing an interface cannot merge members from several parents
Follow-up questions
- →What happens when two extended interfaces declare the same member with incompatible types?
- →How does an interface that extends a class with a
privatemember restrict who can implement it?
SeniorDesignOccasionalA query builder in your codebase has a build() that throws at runtime whenever a required part was never set: it needs a table and at least one selected column. This keeps reaching production. You want the mistake to be a compile error instead. After .from('users') the builder should know the table is set, and build() should not exist as a callable member at all until both required parts are present, while optional calls such as .where() and .orderBy() stay available at any point and may repeat. Describe how you would type the builder so the compiler tracks which parts have been set, what the chain's type becomes after each call, and what happens to the runtime object.
A query builder in your codebase has a build() that throws at runtime whenever a required part was never set: it needs a table and at least one selected column. This keeps reaching production. You want the mistake to be a compile error instead. After .from('users') the builder should know the table is set, and build() should not exist as a callable member at all until both required parts are present, while optional calls such as .where() and .orderBy() stay available at any point and may repeat. Describe how you would type the builder so the compiler tracks which parts have been set, what the chain's type becomes after each call, and what happens to the runtime object.
Carry the parts already set in a type parameter and return a narrowed builder from each call: from() gives Builder<S | 'table'>, select() gives Builder<S | 'cols'>. Gate build() on that accumulator via a this: Builder<'table' | 'cols'> parameter, so an early call fails to compile. The runtime object is unchanged; the parameter is erased.
Common mistakes
- ✗Believing call order cannot be represented in the type system, so it must be a runtime check
- ✗Returning the same builder type from every method, which erases what was already set
- ✗Expecting control-flow analysis of fields to survive across chained method calls
Follow-up questions
- →How would you keep the same guarantee if the builder were immutable and each call returned a new object?
- →What does this design cost in error-message quality when the user calls
build()too early?