skip to content

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%

answer

  1. compile-time only, zero output
  2. the difference is in checking, not emit
  3. method shorthand takes a shortcut
  4. parameters compared in both directions
  5. property form obeys strictFunctionTypes

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.

solid answer

~40 s

They describe the same call shape and they emit nothing at all — an `interface` disappears at compile time, so there is no JavaScript difference to speak of. The difference is purely in how the checker relates them to other types. Parameters of a member written in method shorthand (`onDone(result: string): void`) are compared **bivariantly**: an implementation may narrow the parameter and it still passes. Parameters of the property form (`onDone: (result: string) => void`) are an ordinary function type, and under `strictFunctionTypes` — which `strict` turns on — those are compared **contravariantly**, so narrowing the parameter is an error. So the property form is the stricter, safer way to declare a callback you expect callers to supply; method shorthand is the deliberately lenient one.

go deeper

for a junior

Be ready to say that an interface emits no JavaScript, so both forms describe the same call shape, and that the choice still matters because the compiler checks their parameters differently.

for a middle

Explain the mechanics: method-shorthand parameters are compared in either direction, while a property function type's parameters are compared contravariantly under strictFunctionTypes, and return types are always covariant.

for a senior

Show you pick per situation — property form for callbacks users supply so mismatches fail at compile time, method shorthand where the loose check keeps a container-like type usable — and that you know the target declaration decides.

for a principal

Own the consistency question: decide a house form for callback members across a shared codebase or published types, and be able to justify the migration cost against the class of runtime bugs it removes.

## Two ways to write one member Inside an `interface` or an object type alias, a member whose value is a function can be declared in two syntaxes: ```ts interface Job { run(id: string): void; // method shorthand } interface Job2 { run: (id: string) => void; // property holding a function type } ``` Both say the same thing in plain English: an object of this type has a member called `run`, you call it with one string, and you should ignore the result. Any object literal, class instance, or imported value with a suitable `run` satisfies either declaration — TypeScript is structurally typed, so no `implements` clause is needed for the shapes to line up. ## Nothing survives compilation The first thing to say in an interview is that neither form generates code. TypeScript's type layer is erased: `interface` declarations produce zero output, so the emitted JavaScript for a file containing `Job` and a file containing `Job2` is byte-for-byte the same. There is no runtime object named `Job`, nothing to `instanceof`, and no per-call check that the argument really is a string. Everything the two forms do, they do to the compiler. ## Where they actually differ The difference shows up only when the compiler has to decide whether one type is assignable to another, and only in the **parameters**. ```ts interface MethodForm { handle(e: Event): void } interface PropForm { handle: (e: Event) => void } const a: MethodForm = { handle(e: MouseEvent) {} }; // OK // const b: PropForm = { handle(e: MouseEvent) {} }; // Error ``` For the method-shorthand member, TypeScript compares the parameter types **bivariantly** — the relation is accepted if either type is assignable to the other. `MouseEvent` is assignable to `Event`, so the narrower implementation is waved through, even though calling `a.handle(someKeyboardEvent)` would hand the implementation something it does not expect. For the property form, the member is an ordinary function type, and `strictFunctionTypes` (switched on as part of `strict`) compares parameters **contravariantly**: the target's parameter must be assignable to the source's parameter. `Event` is not assignable to `MouseEvent`, so the assignment is rejected. Return types are unaffected — they are always compared covariantly in both forms. One detail worth knowing: it is the **declared (target) type's** form that decides. Writing the implementation as a method does not buy back the loose check when the type it must satisfy declares a property function type — the commented-out `b` above is still an error. ## A class body is a different question The "no runtime difference" claim is specific to interfaces and object type aliases. Inside a class body, `handle(e: Event) {}` and `handle = (e: Event) => {}` are genuinely different JavaScript: the first lands on the prototype and is shared by all instances, the second is a per-instance field whose arrow function captures `this` lexically. That is a JavaScript-semantics choice about `this` and memory, layered on top of the type-checking difference described here. ## Which one to write A practical default: use the **property form** for callbacks your callers supply — options bags, event handler fields, dependency-injection hooks — because you want a mismatched parameter type to be a compile error rather than a surprise at runtime. Use **method shorthand** for members of container-like or generic types where the loose check is what keeps the type usable, and for members you expect many types to implement with slightly different parameter types on purpose. Teams often pick one form as the house style and let a linter enforce it, precisely because the two look interchangeable but are not.

  • Does the same reasoning hold inside a class body?
    No. In a class, `handle(e) {}` and `handle = (e) => {}` really do differ at runtime: the first is a prototype method shared by instances, the second a per-instance field whose arrow captures `this` lexically. In an interface there is no runtime object at all, so only the checking rules differ.
  • If I write my implementation as a method, do I get the relaxed check even when the declared type uses the property form?
    No — the declared target type's form decides. `const h: { f: (e: Event) => void } = { f(e: MouseEvent) {} }` is still an error under `strict`. Writing the source as method shorthand does not recover the bivariant comparison.

saying these in an interview costs you the question

  • Claims the two forms compile to different JavaScript
  • Says the choice is pure style with no checking difference
  • Thinks an interface exists at runtime and can be checked with instanceof
  • Believes strict mode makes every parameter position contravariant
  • Assumes the implementation's syntax, not the declared type's, decides the check

context