skip to content

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%

answer

  1. the declaring side chooses the strictness
  2. who supplies the function decides the form
  3. containers need the looser rule to stay usable
  4. loud break now versus silent bug later
  5. one house form, enforced, with named exceptions

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.

solid answer

~50 s

Treat it as an API policy, not a style vote. For callbacks a consumer *supplies* — options bags, event hooks, plugin handlers — use the property function-type form: parameters are then compared contravariantly, so a consumer who types a handler more narrowly than the contract gets a compile error instead of a runtime surprise. Reserve method shorthand for members of container-like or generic types where bivariance is what keeps the type assignable at all, and for contracts you expect many implementers to satisfy with intentionally varying parameter types. The commitment is about evolution: with the property form, widening a parameter later is a *loud* break — every narrower consumer handler stops compiling, which is exactly the signal you want. With method shorthand the same widening is silent, and lands as a production bug in someone else's code. Pick one, document it, and enforce it mechanically.

go deeper

for a junior

Know that library authors choose how strictly your callbacks get checked, and that a handler declared with a narrower parameter than the library promises can slip through.

for a middle

Explain that the property form is compared contravariantly while method shorthand is not, so the declaration form decides whether a mismatched consumer handler is a compile error.

for a senior

Argue a default per member kind, keep the exception for container-like generic types, and show the runtime failure mode the looser form produces in a real integration.

for a principal

Own the release consequence: the declaration form determines whether a future parameter widening is a loud semver-major break or a silent runtime bug in consumer code, and back the policy with a documented default and mechanical enforcement.

## The decision, restated Every function-valued member in your published types is declared one of two ways: ```ts interface Options { onEvent(e: Event): void; // method shorthand — bivariant parameters onEvent2: (e: Event) => void; // property function type — contravariant under strict } ``` They emit nothing and describe the same call shape. What differs is how strictly the compiler checks what your consumers write, and — because the *declared* type is what decides — that strictness is your choice to make on their behalf, not theirs. ## Default: property form for anything the consumer supplies When a consumer passes you a handler, your type is a contract about what *you* may call it with. Under the property form, `strictFunctionTypes` enforces that contract: a consumer who writes `onEvent: (e: MouseEvent) => {}` against a declared `Event` parameter gets an error at the line they wrote, with their own names in the message. Under method shorthand the same code compiles, your library dispatches a `KeyboardEvent` into it, and the failure arrives as an `undefined` property somewhere unrelated. That difference — error at the definition site versus a mystery at runtime — is the whole argument. ## Where method shorthand still earns its place Two cases: 1. **Container-like and generic types.** If your interface is `Collection<T>` with `add(item: T): void` and `contains(item: T): boolean`, the honest structural answer is invariance in `T`, and invariance makes the type painful to pass around. Method shorthand keeps `Collection<Dog>` usable where `Collection<Animal>` is expected — the same bargain the standard library makes with `Array<T>`. 2. **Broad contracts with intentionally varying implementations.** Visitor- and plugin-shaped interfaces that many outside types satisfy, where implementers legitimately handle a narrower case and you have another mechanism (a discriminant, a registration key) that actually guarantees the right value arrives. In both cases you are choosing to trust the implementer. Say so in the docs. ## The evolution commitment This is the part that separates a policy from a preference. Consider widening a published parameter from `MouseEvent` to `Event`, a normal thing to do when a callback gains new call sites. - **Property form:** every consumer handler typed to the old narrow parameter now fails to compile. That is a **breaking change** in the semver sense, and it should be — those handlers really cannot cope with the new inputs. You ship it in a major, with a migration note. - **Method shorthand:** nothing breaks at compile time. Consumers upgrade in a patch release and their handlers start receiving events they were never written for. You have converted a compile break into a distributed, hard-to-attribute runtime bug in code you do not own. The property form does not create the breakage; it *reveals* it. A library whose types cannot express that a change is breaking will make its users pay for it later. Some changes are non-breaking under both forms and worth knowing: **adding** a parameter to a callback you invoke is safe, because a function that accepts fewer parameters is assignable to one that accepts more — consumers simply ignore the new argument. **Narrowing** a parameter is the mirror case and is safe under the property form. ## Making the policy stick A mixed surface is the worst outcome: consumers cannot predict which members check strictly, and neither can your own contributors. Pick a default, write it into the contribution guide with the one-sentence reason, and enforce it mechanically — the `@typescript-eslint/method-signature-style` rule exists exactly to pin one form across a codebase, with escape comments for the container-like exceptions. Migrating an existing surface is mostly mechanical, but expect the switch to the property form to surface real consumer errors on the first release that ships it; budget that as part of the change rather than reverting when the first bug report arrives. ## What to say in the interview Name the mechanism (bivariant methods versus contravariant function types), state a default with a reason, name the exception class, and — the part most candidates miss — connect it to release policy: the declaration form decides whether your future parameter changes are enforceable at compile time or merely hoped for.

  • Is adding a parameter to a published callback a breaking change under either form?
    No. A function that accepts fewer parameters is assignable to one that accepts more, so existing consumer handlers keep compiling and simply ignore the new argument. The dangerous direction is widening an existing parameter's type, which the property form reports and method shorthand hides.
  • How would you migrate an existing surface from method shorthand to the property form?
    Convert in one pass, ship behind a major version, and expect real consumer errors on first release — those are pre-existing bugs the old form was hiding, not regressions. Keep documented exceptions for container-like generics, and pin the chosen form with a lint rule so the surface does not drift back.
  • Does this policy change anything about what you ship at runtime?
    Nothing at all. Both forms are erased — an interface emits no JavaScript either way, and the declaration form has no effect on bundle size or execution. The entire decision is about which errors your consumers see at compile time.

saying these in an interview costs you the question

  • Treats the choice as pure formatting with no consequence
  • Assumes the consumer's own syntax controls how strictly they are checked
  • Thinks the property form makes the runtime call safer
  • Calls the resulting compile errors a regression rather than revealed bugs
  • Applies one form everywhere without excepting container-like generics

context