In TypeScript, what does `interface Handle extends Connection {}` mean when `Connection` is a class with a private field, and which types can then satisfy `Handle`?
answer
- interfaces may extend class types
- member types come across, bodies do not
- private members compare by declaration site
- structurally identical is not enough
- only the class family qualifies
basics
~20 sAn interface may extend a class type: it inherits the member types, including private and protected ones, but no implementations. Because private members are compared by declaration site, only that class and its subclasses can produce a type assignable to the interface.
solid answer
~50 sAn interface is allowed to extend a class, and when it does it inherits the class's member *types* — including the private and protected ones — while inheriting none of the implementations. That inheritance is what makes the result unusual: private and protected members are compared nominally, by the declaration they came from, not structurally. So a hand-written object or an unrelated class with an identically named private field is rejected; the compiler says the property is private in one type and not the other, or that the two types have separate declarations of it. The only types that satisfy `Handle` are `Connection` itself and its subclasses, since they inherit the very same private declaration. It is one of the few genuinely nominal corners of a structural type system, and teams use it deliberately to keep an interface closed to outside implementations.
code
typescript · 29 linesclass Connection {
private socketId = 0;
host = "localhost";
send(data: string): void {
void data;
void this.socketId;
}
}
// Inherits Connection's member types, including the private one
interface Handle extends Connection {
close(): void;
}
// Works: a subclass inherits the very same private declaration
class Pooled extends Connection implements Handle {
close(): void {}
}
// Fails: separate declaration of a private property
// class Fake implements Handle {
// private socketId = 0;
// host = "x";
// send(data: string): void {}
// close(): void {}
// }
const h: Handle = new Pooled();
console.log(h.host);go deeper
Know that an interface is allowed to extend a class and that doing so borrows the class's member types, not its code.
Explain the mechanics that make it closed: private and protected members are compared by the declaration they came from, so a structurally identical object or unrelated class is rejected while subclasses pass.
Recognise the pattern as a deliberate way to seal an interface to one hierarchy, read its error messages fluently, and weigh the mocking cost and the silent loss of the guarantee if the private member is ever removed.
Decide whether a closed extension point is worth constraining every consumer and test double, and whether the invariant would be better protected by a factory boundary or a run-time check that survives erasure.
## Interfaces can extend classes Most people meet `extends` on an interface as a way to build on another interface. It also accepts a **class**, and the rule is precise: the interface inherits the class's members' *types* but not their implementations. The static side and the constructor are not inherited — only the instance shape. ```ts class Connection { private socketId = 0; host = "localhost"; send(data: string): void {} } interface Handle extends Connection { close(): void; // adds a requirement on top } ``` `Handle` now requires `host`, `send`, `close` — and `socketId`, which nobody outside `Connection` can write. ## Why that closes the interface TypeScript's assignability is structural: two types are compatible when their members line up, regardless of lineage. Private and protected members are the deliberate exception. They are compared **by declaration site**: two types are compatible with respect to a private member only if the member came from the *same* declaration in the *same* class. A different class that declares its own `private socketId` is a different declaration and therefore incompatible. Combine the two facts and the consequence is unavoidable: ```ts // Rejected: the object literal has no way to have Connection's private member // const h: Handle = { host: "x", send() {}, close() {} }; // Rejected: separate declaration of a private property // class Fake implements Handle { // private socketId = 0; // host = "x"; // send(d: string) {} // close() {} // } // Accepted: inherits the same private declaration class Pooled extends Connection implements Handle { close(): void {} } ``` The error text varies with what the candidate type looks like: if it has no such member you get "property is missing"; if it has a public one you get "property is private in one type but not in the other"; if it declares its own private one you get "types have separate declarations of a private property". All three are the same rule reported from different angles. ## Why anyone would want this Structural typing is normally what you want, but occasionally you need to state "only my class family may satisfy this": - **A closed extension point.** A framework exposes `Handle` in its public types so consumers can *hold* one, while ensuring the only way to obtain one is through the framework's own class hierarchy. Third parties cannot hand-roll a substitute that skips invariants the base constructor establishes. - **Preventing accidental compatibility.** Two unrelated domain types with the same shape are interchangeable under structural typing. A private member makes them distinct, which is the same instinct behind other nominal-emulation techniques in TypeScript, applied here through the class. - **Documenting a base requirement.** The interface reads as "a `Connection` that also closes", which is sometimes clearer than repeating the base's members. ## The costs, which the interviewer usually probes next 1. **Tests and mocks get hard.** A test double cannot implement `Handle` without extending the real class, so it drags in whatever the base constructor does. That is often the reason a team regrets the pattern. 2. **The private member is invisible but load-bearing.** Someone reading `Handle` sees a normal-looking interface; the closure comes from a member they cannot see and did not write. Comment it, or the next maintainer will spend an afternoon on the error message. 3. **Removing the private field silently opens the gate.** If a later refactor deletes or publicises `socketId`, `Handle` quietly becomes structurally satisfiable by anything of the right shape — the guarantee disappears with no error anywhere. 4. **It is compile-time only.** Everything above is erased. Nothing stops JavaScript callers, and nothing checks at run time that a value came from the right family. ## The rule to state in an interview "An interface may extend a class and inherits its member types, including private and protected ones, but no implementations; and because private and protected members are compared by declaration rather than structurally, only that class and its subclasses can be assignable to the result." Then add the practical half: it is a deliberate way to close an interface, it is invisible to readers, and it makes mocking painful — so use it where a closed hierarchy is the actual requirement, not as a habit.
- Does the interface also inherit the class's static members or its constructor?No. Extending a class in an interface brings across the instance shape only — the members you would find on an object produced by the class. The constructor and everything on the class side are not part of it, which is consistent with how conformance checking treats classes generally: it is always the instance half that is compared.
- What breaks in tests when an interface is closed this way?Hand-written doubles stop compiling, because a plain object can never carry the base's private declaration. Your options are extending the real class in the test — inheriting whatever its constructor does — or asserting a stub through an unsafe cast, which discards the checking you added the pattern for. That friction is the usual reason teams back it out.
- What happens to the guarantee if someone later makes the private field public?It disappears silently. With no private member left, comparison goes back to being purely structural, so any object of the right shape satisfies the interface again — and nothing anywhere reports a change. That fragility is worth a comment on the field explaining that its privacy is load-bearing, not incidental.
saying these in an interview costs you the question
- Says an interface cannot extend a class
- Thinks the interface inherits the class's method implementations
- Believes a class with its own identically named private field qualifies
- Assumes only the exact class qualifies, never its subclasses
- Treats the closure as a run-time guarantee