Given `interface Logger { (message: string): void; level: number }` in TypeScript, why does `const logger: Logger = (message) => {}` fail to compile, and how do you build a conforming value without an `as` assertion?
answer
- the annotation checks the initializer
- a bare arrow has no properties
- assign returns an intersection
- properties fold into an inferred function type
- an assertion checks nothing
basics
~20 sAn arrow function satisfies the call signature but has no level property, so the initializer is missing a member. Build the value with Object.assign, whose result type is the callable intersected with the properties, or assign properties onto a function declaration so the compiler folds them into its type.
solid answer
~50 sThe annotation is checked against the **initializer**, and an arrow function expression is callable but has no `level`, so the compiler reports the missing property. Two ways to fix it honestly. First, `Object.assign(fn, { level: 1 })` — its declared return type is the intersection of the target and the sources, so the result is callable *and* carries `level`, and the assignment checks. Second, declare the function with `function` (or a `const`-bound function expression) and assign the property afterwards; the compiler folds properties assigned in that scope into the function's inferred type, and the result is then assignable to `Logger`. The third option — `as Logger` on the bare arrow — is not a fix: an assertion performs no check and emits no code, so if you forget to attach `level`, reads of it return `undefined` at runtime with the type layer insisting otherwise.
code
typescript · 13 linesinterface Logger {
(message: string): void;
level: number;
}
const logger: Logger = Object.assign(
(message: string): void => {
if (logger.level > 0) console.log(message);
},
{ level: 1 },
);
logger('checked, not asserted');go deeper
Understand that a value annotated with a callable-plus-properties type must have the properties too — a plain arrow function is only half of it.
Explain that the annotation is checked at the initializer, and produce a conforming value with Object.assign or by assigning properties onto a function declaration.
Argue why the assertion route is a defect: it removes the check while keeping the claim, so a missing property becomes a silent undefined far from its cause. Know the widening trap on literal-typed properties.
Set the codebase rule — where assertions are permitted at all, how construction of hybrid values is confined to small factories with honest return types, and how that boundary is enforced in review.
## Why the obvious code is rejected ```ts interface Logger { (message: string): void; level: number; } const logger: Logger = (message) => {}; // Property 'level' is missing in type '(message: string) => void' ``` The annotation on a `const` is checked against the initializer at the point of declaration. The arrow function matches the call signature, and nothing else. Adding `logger.level = 1` on the next line does not help — the error is at the initializer, and by then `logger` is already required to have been a full `Logger`. This surprises people because the JavaScript would work fine: functions are objects, you can hang a property on one at any time. The type layer is stricter than the runtime here, deliberately: it wants the value to be complete when it claims the type. ## Fix one: `Object.assign` The standard library types `Object.assign` so that the result is the intersection of the target and the sources: ```ts const logger: Logger = Object.assign( (message: string): void => { if (logger.level > 0) console.log(message); }, { level: 1 }, ); ``` The expression's type is `((message: string) => void) & { level: number }`, which is assignable to `Logger`. Nothing is asserted; if you misspell `level` or give it a string, the assignment fails to compile — which is precisely the property you want. One sharp edge: an object literal widens its property types. If `Logger` declared `level: 'debug' | 'info'`, then `{ level: 'info' }` infers `level: string` and the assignment is rejected. Fix it by narrowing at the source — `{ level: 'info' as const }` — or by annotating the literal. ## Fix two: properties on a function declaration TypeScript folds properties assigned to a function declaration (or a `const`-bound function expression) into that function's inferred type, within the same scope: ```ts interface Counter { (label: string): number; count: number; } function tick(label: string): number { tick.count += 1; console.log(label, tick.count); return tick.count; } tick.count = 0; const counter: Counter = tick; // ok ``` Here `tick`'s inferred type already includes `count`, so the final assignment checks. The property's type comes from the assignment, so the same widening caveat applies: `tick.level = 'info'` infers `string`. This form reads well when the hybrid is a module-level singleton; `Object.assign` reads better when you are producing values in a factory. ## Why the assertion is the wrong answer ```ts const logger = ((message: string) => {}) as Logger; // compiles logger.level.toFixed(0); // runtime TypeError ``` `as` is an assertion, not a conversion. It performs no check, generates no code, and simply instructs the compiler to stop disagreeing. The whole point of the hybrid type was to guarantee that callers can read `level`; asserting deletes that guarantee while leaving the documentation in place, which is worse than having no type at all — the next reader trusts it. There is a narrow legitimate use: building the value in steps where the intermediate state genuinely cannot be expressed, ideally isolated inside one small factory function whose *return* type is the honest one, so the unchecked region is three lines rather than a whole module. ## Choosing between the two fixes - **Factory or per-instance value** → `Object.assign`, because it composes and its type falls out of inference. - **Module-level singleton with self-reference** → function declaration plus property assignment, because the function can refer to its own properties naturally and hoisting makes ordering forgiving. - **Value produced by a third-party call** → wrap it in a factory that returns the hybrid type, and keep any assertion inside. ## The runtime picture None of this exists after compilation. `Object.assign` is a real runtime call and really does attach the property; the function-declaration form emits a plain property assignment. The *interface* emits nothing, checks nothing, and defends nothing — it only decided, at compile time, whether your construction was honest. That asymmetry is exactly what an interviewer is probing with this question: can you produce a value that genuinely has the shape, rather than one the compiler has been told to stop asking about.
- Why is `as Logger` on the bare arrow function dangerous here?Because `as` asserts rather than converts: no check runs and no code is emitted. The value has no `level`, so `logger.level` is `undefined` at runtime while the type says `number` — and `undefined > 0` quietly evaluates to `false` rather than throwing, so the bug surfaces far from its cause. The type is now a false document.
- Suppose `level` is typed `'debug' | 'info'` instead of `number` — what changes with `Object.assign`?The object literal widens: `{ level: 'info' }` infers `level: string`, which is not assignable to the union, so the assignment is rejected. Narrow at the source with `{ level: 'info' as const }`, or annotate the literal so the property keeps its literal type. The same widening applies to properties assigned onto a function declaration.
- Does adding `logger.level = 1` on the line after the declaration fix the original error?No. The annotation is checked against the initializer, so the error is reported where the value is created — before any later statement runs. The value must already be complete when it claims the type; a later assignment is a separate operation the declaration check knows nothing about.
saying these in an interview costs you the question
- Says the arrow function satisfies the interface because it is callable
- Reaches for `as Logger` and calls the error a TypeScript limitation
- Thinks assigning the property on the next line fixes the initializer error
- Assumes `Object.assign` returns only the target's type, losing the properties
- Believes the interface guarantees at runtime that `level` exists