In TypeScript, `interface Options { retries: number; onError?: () => void }` and `class Config implements Options { retries = 3 }` — does that compile, and what goes wrong later when code calls `config.onError?.()`?
answer
- optional means absence is allowed
- the clause adds nothing to the class
- class type comes from the class body only
- same object, different type, different answer
- declare the optional member yourself
basics
~20 sIt compiles: an optional member is satisfied by absence. But the implements clause adds nothing to the class, so the type Config has no onError at all, and reading it through a Config-typed value is a compile error. Declare it explicitly.
solid answer
~40 sIt compiles. `onError` is optional, so the class satisfies `Options` without declaring it — and because `implements` only checks the class and never modifies it, `Config` genuinely has no `onError` member. The consequence bites later: `this.onError` inside the class, and `config.onError?.()` where `config` is typed `Config`, both fail with "property does not exist". Oddly, the same object works fine when it is typed `Options`, because that type does declare the optional member — so whether the call compiles depends on which type the value is annotated with at that site. The fix is to write `onError?: () => void` in the class body yourself. This is the general rule showing up again: the clause is a predicate, not a mixin.
code
typescript · 21 linesinterface Options {
retries: number;
onError?: () => void;
}
class Config implements Options {
retries = 3;
}
const c = new Config();
// c.onError?.(); // error: Property 'onError' does not exist on type 'Config'
const asOptions: Options = c;
asOptions.onError?.(); // fine: Options declares the optional member
class FixedConfig implements Options {
retries = 3;
onError?: () => void; // declared, so it is part of the class type
}
new FixedConfig().onError?.();go deeper
Remember that an optional interface member does not have to appear in a class that implements the interface, and that you can only use a property the class itself declares.
Explain that the class type is exactly its own declarations, so the optional member is genuinely absent, and show that access is decided by the declared type of the expression rather than by the underlying object.
Anticipate the inconsistent errors this causes across a codebase, where the same instance works through an interface-typed parameter and fails through the inferred class type, and fix it at the class declaration rather than by sprinkling assertions.
Weigh whether the class form earns its duplication at all: contracts with many optional members are usually better served by a typed object or factory, and mandating explicit declaration keeps the public surface readable to consumers.
## What actually happens The code compiles cleanly. Two separate rules combine to produce a result that surprises people: 1. **An optional member is satisfied by absence.** `onError?: () => void` means "this property may be missing", so a type without it is still assignable to `Options`. `implements` asks only about assignability, and the answer is yes. 2. **The clause does not change the class.** `Config`'s type is exactly what its body declares: one property, `retries: number`. `onError` never becomes part of it. So you have a class that legitimately implements the interface while not having one of the interface's members — and the type system agrees with both statements at once. ## Where it goes wrong ```ts interface Options { retries: number; onError?: () => void; } class Config implements Options { retries = 3; fail() { // Error: Property 'onError' does not exist on type 'Config'. // this.onError?.(); } } const c = new Config(); // c.onError?.(); // same error — Config has no such member const asOptions: Options = c; asOptions.onError?.(); // fine — Options declares it ``` Note the last two lines carefully: **the very same object** is accepted or rejected depending on the type of the variable holding it. That is not a contradiction, it is structural typing doing its job — `c` is a valid `Options`, and `Options` says the property may exist, so optional access is allowed through that view. Through the `Config` view there is no such property to talk about. In a real codebase this shows up as an inconsistent error: the call site that receives the value as a parameter typed `Options` compiles, while the one that constructs the class directly and keeps the inferred `Config` type does not. ## The runtime angle At run time none of this exists. The instance simply has no `onError` property, so `c.onError` would be `undefined` and optional chaining would short-circuit — exactly what the author intended. The compiler is refusing on a *declaration* basis, not a value basis, which is why the error feels pedantic when you first hit it. Turning the diagnostic into working code is a one-line declaration, not a redesign. ## The fix Declare the member in the class: ```ts class Config implements Options { retries = 3; onError?: () => void; // now part of Config's type } ``` That single line makes `this.onError?.()` legal inside the class, makes `c.onError` legal outside it, and keeps the property genuinely optional so nobody is forced to assign it. If the class always has a handler, drop the `?` and give it an initializer — a class is allowed to be *more* specific than the contract it satisfies, because a required property is assignable to an optional one. ## The mirror-image gaps The same "it only checks" rule produces three more results worth knowing together: - **Extra members are fine.** A class may declare properties the interface never mentions; the check is one-directional assignability, and the class is free to be richer than the contract. - **A narrower member type is fine.** Declaring `retries: 3` when the interface says `number` satisfies it; the reverse does not. - **A missing *required* member is not fine.** Drop `retries` and you get an error on the class heading naming the missing property. That is the one case the clause reliably catches, and it is the reason to write it at all. ## How to say it in an interview The crisp formulation is: *`implements` is a predicate, not a mixin.* It answers "is this class assignable to that type?" and then goes away. Absence of an optional member makes the predicate true, so nothing is reported — and because the clause adds no members, the absence is real everywhere the class type is used. If you want the member on the class, put it on the class. A related habit worth mentioning: when a type has several optional members and you want the class to expose all of them, declaring them explicitly also documents the class's surface for readers, who otherwise have to open the interface to know what is even possible. The duplication is the price of the class form; that is also a fair argument for using a plain object typed with the interface when you do not need instances.
- Why does the optional call compile when the value is typed as the interface but not when it is typed as the class?Because access is checked against the *declared type of the expression*, not the object behind it. The interface declares `onError` as possibly present, so optional access is legal through that view. The class type never declared it, so through that view there is no such property to access. Same object, two different static views.
- If the class always provides the handler, should it still declare the member as optional?No — declare it required with an initializer. A required property is assignable to an optional one, so the class still satisfies the interface, and callers holding the class type get a non-optional member they need not guard. Being more specific than the contract is allowed; being less specific is not.
saying these in an interview costs you the question
- Expects the class to inherit the interface's optional members
- Thinks the missing optional member is a conformance error
- Says the property exists at run time because the interface declares it
- Believes a class may not declare members beyond the interface
- Assumes the error means the implements clause is wrong