skip to content

Method vs Property Function Typing

The famous TypeScript wrinkle: writing a function member as a method shorthand checks its parameters bivariantly, while the property form is checked contravariantly under strictFunctionTypes. It explains why some obviously unsound assignments compile.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

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?

level: middleimportance: must knowfreq 52%

answer

  1. two comparison rules, one member kind
  2. either direction versus one direction
  3. the flag skips method declarations
  4. the declared target type chooses
  5. narrowing a parameter is the unsound move

basics

~20 s

TypeScript 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 s

The 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 lines
typescript
interface 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

for a junior

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.

for a middle

Explain both comparison rules by name and direction, say that strictFunctionTypes exempts method and constructor declarations, and note that return types stay covariant throughout.

for a senior

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.

for a principal

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

context

open as a page

In a TypeScript interface, a callback member can be written as `onDone(result: string): void` or as `onDone: (result: string) => void`. Do the two forms differ in the emitted JavaScript, and are they interchangeable?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Neither form emits anything: interfaces are erased, so the JavaScript is identical. They are not fully interchangeable to the checker, which compares method-shorthand parameters loosely in both directions but compares a property function type's parameters strictly under strict mode.

open as a page

Why did TypeScript's designers leave method parameters bivariant instead of checking them contravariantly like other function types, and what unsoundness does that leave in everyday code?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Contravariant method parameters would make generic types unusable: Array puts its element type in parameter positions such as push and indexOf, so an array of a subtype would stop being assignable to an array of its supertype. TypeScript traded soundness for usability.

open as a page

You own a widely used TypeScript library whose public types declare dozens of callback members. How do you decide between method shorthand and the property function-type form across that surface, and what does each choice commit you to?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Declare consumer-supplied callbacks as property function types so parameter mismatches fail at compile time, and reserve method shorthand for container-like types whose usability depends on the looser check. The choice fixes whether future parameter widening breaks consumers loudly or fails silently at runtime.

open as a page