Under `strict`, TypeScript accepts `const h: { handle(e: Event): void } = { handle(e: MouseEvent) {} }` but rejects the same object when the target type is written `{ handle: (e: Event) => void }`. Why?
answer
- two comparison rules, one member kind
- either direction versus one direction
- the flag skips method declarations
- the declared target type chooses
- narrowing a parameter is the unsound move
basics
~20 sTypeScript compares parameters of members declared with method shorthand bivariantly, so the narrower MouseEvent parameter is accepted. The property form is a plain function type, and strictFunctionTypes, which strict enables, compares those parameters contravariantly, making the narrowing an error.
solid answer
~40 sThe two targets select different comparison rules. When the target member is written as method shorthand, TypeScript relates the parameter types **bivariantly** — the relation passes if either parameter type is assignable to the other — so a `MouseEvent` parameter satisfies a declared `Event` parameter. When the target member is a property whose type is a function type, `strictFunctionTypes` (turned on by `strict`) applies the sound rule: parameters are **contravariant**, so the declared `Event` must be assignable to the implementation's `MouseEvent`, which it is not. Note that the *target* declaration decides — writing the source as a method does not recover the loose check. Method bivariance is a deliberate exemption, not an oversight, and it is genuinely unsound: `h.handle(new KeyboardEvent("keydown"))` type-checks and then reads properties the object does not have.
code
typescript · 14 linesinterface MethodForm { handle(e: Event): void }
interface PropForm { handle: (e: Event) => void }
// Method shorthand target: parameters compared bivariantly.
const a: MethodForm = { handle(e: MouseEvent) { console.log(e.clientX); } };
// Property target: strictFunctionTypes compares parameters contravariantly.
// @ts-expect-error MouseEvent parameter is narrower than the declared Event
const b: PropForm = { handle(e: MouseEvent) { console.log(e.clientX); } };
// The unsoundness the method form lets through:
a.handle(new KeyboardEvent("keydown")); // compiles; clientX is undefined
export { a, b };go deeper
Know that the two ways of declaring a function member are not equivalent to the checker, and that a narrower parameter slips through the method form but not the property form.
Explain both comparison rules by name and direction, say that strictFunctionTypes exempts method and constructor declarations, and note that return types stay covariant throughout.
Demonstrate the consequence: an implementation that narrows a parameter compiles and then reads a property that is not there at runtime. Show the guard-inside-the-body fix rather than reaching for an assertion.
Decide where the leniency is worth its cost across a shared type surface, and be able to say why disabling strictFunctionTypes to silence errors makes the codebase strictly less safe.
## What the two rules are Assignability between function types comes down to two questions: can the target's return type be produced, and can the target's parameters be accepted? - **Return types are covariant**, always. A function returning `Dog` is assignable where a function returning `Animal` is expected, because every value it hands back is a valid `Animal`. - **Parameters are the interesting half.** The sound rule is **contravariance**: a function is only safe as a replacement if it accepts *everything* the declared type promised callers could pass. So `(e: Event) => void` is assignable to `(e: MouseEvent) => void` — it copes with more — but not the other way round. TypeScript applies the sound, contravariant rule only when `strictFunctionTypes` is enabled (it is part of the `strict` family), and only to function types that did **not** originate in a method or constructor declaration. Everything else — method shorthand in an interface, object type or class, and constructor signatures — is compared **bivariantly**: the check passes if the parameters are assignable in *either* direction. ## Walking the example ```ts interface MethodForm { handle(e: Event): void } interface PropForm { handle: (e: Event) => void } const a: MethodForm = { handle(e: MouseEvent) { console.log(e.clientX); } }; // OK: bivariant — MouseEvent is assignable to Event, so the relation passes. // const b: PropForm = { handle(e: MouseEvent) {} }; // Error 2322: contravariant — Event is not assignable to MouseEvent. ``` For `a`, the target member `handle` is a method signature. The checker sees a method declaration on the target and skips the strict variance path, so it accepts the pair as long as one direction works. `MouseEvent extends Event`, so one direction works, and the assignment compiles. For `b`, the target member is a property signature whose type happens to be a function type. No method declaration is involved, so `strictFunctionTypes` applies the contravariant rule and the assignment fails. ## The target declaration is what decides A common misreading is that the *implementation's* syntax matters. It does not. In the `b` case the source is written with method shorthand (`{ handle(e: MouseEvent) {} }`) and it is still rejected, because the declared type it is being checked against uses the property form. If you want the loose check, the *declared* type has to be the method form. Practically, this means the author of the interface — not the author of the implementation — chooses how strictly callbacks get checked. ## Why the loose rule exists at all Bivariant methods are a deliberate concession. Making method parameters contravariant would immediately break generic container types: `Array<T>` declares `push(...items: T[]): void`, `indexOf(searchElement: T): number` and friends, all of which put `T` in a parameter position. Under a contravariant rule, `Dog[]` would no longer be assignable to `Animal[]`, and an enormous amount of ordinary, mostly-correct code would stop compiling. TypeScript chose usability over soundness here and documents it as such. ## The hole it leaves The hole is real and worth demonstrating in an interview: ```ts const a: MethodForm = { handle(e: MouseEvent) { console.log(e.clientX); } }; a.handle(new KeyboardEvent("keydown")); // type-checks; clientX is undefined ``` Every caller of `a` is entitled by the type to pass any `Event`. The implementation assumed a `MouseEvent`. Nothing at runtime enforces the difference, because types are erased. The failure is not a `TypeError` at the boundary — it is a silently `undefined` property, or a crash several frames deeper. ## What to do about it - Declare callbacks that consumers supply as **property function types**, so a narrower parameter is a compile error at the point where it is written. - Keep **method shorthand** where the type is container-like or where many implementers legitimately vary the parameter type, and accept that you are trusting them. - If a handler really only wants a subset, take the wide type and narrow inside the body with a guard such as `if (e instanceof MouseEvent)`. That produces a check that actually exists at runtime, rather than a promise the compiler cannot keep. - Turning `strictFunctionTypes` off returns *everything* to bivariance, which is strictly worse; the flag is not the source of the problem.
- Does writing the implementation as a method rescue it when the declared type uses the property form?No. The target declaration decides. `const h: { f: (e: Event) => void } = { f(e: MouseEvent) {} }` still errors under `strict`, because the checker looks at how the type being assigned *to* declares the member, not how the source is written.
- Does strictFunctionTypes change how return types are compared?No. Return types are compared covariantly in every mode: a function returning a subtype is assignable where a supertype return is expected. `strictFunctionTypes` only changes parameter comparison, and only for function types that did not originate in a method or constructor declaration.
- What happens to both examples if strictFunctionTypes is switched off?Both compile. With the flag off, all function types fall back to bivariant parameter comparison, so the property form loses its protection too. Turning the flag on tightens only the non-method forms; method and constructor declarations stay bivariant either way.
saying these in an interview costs you the question
- Says strict mode makes all parameters contravariant, methods included
- Claims the implementation's syntax decides which rule applies
- Thinks the method form performs a runtime check on the argument
- Confuses the rule with return types, which are always covariant
- Calls method bivariance a compiler bug rather than a deliberate exemption