In TypeScript, how do you type a value that is both callable and carries properties — for example a `log(message)` function that also has `log.level` and `log.reset()` — and why can't the `(message: string) => void` shorthand express it?
answer
- functions are objects too
- the arrow form is closed
- move the callable into a member list
- colon instead of arrow, plus properties
- intersection needs the parentheses
basics
~20 sDeclare the callable as a call-signature member of an object type or interface, alongside the properties. The arrow shorthand is one closed type expression with nowhere to put extra members; an intersection of a function type and an object type is the equivalent.
solid answer
~50 sUse the **member form** of a call signature. Inside an interface or an object type you write the callable as `(message: string): void;` — colon, not arrow — and then list `level: number` and `reset(): void` beside it. Such a type is usually called a *hybrid type*, and it describes real library surfaces: a function you can call that also hangs configuration and helpers off itself. The `(message: string) => void` shorthand cannot express it because it is a single, closed type expression — the return type absorbs everything to the right of the arrow, so there is no member list to extend. The other spelling is an intersection: `((message: string) => void) & { level: number; reset(): void }`, which type aliases and interfaces both accept. Nothing here is interface-only; a type alias can hold call-signature members too. And as always the whole description is erased at compile time.
code
typescript · 21 linesinterface Logger {
(message: string): void;
level: number;
reset(): void;
}
// Same shape written as an alias with an intersection.
type LoggerAlias = ((message: string) => void) & {
level: number;
reset(): void;
};
declare const log: Logger;
declare const alt: LoggerAlias;
log('starting');
log.level = 2;
log.reset();
alt('starting');
alt.level = 0;go deeper
Know that in TypeScript a function can also carry properties, and that describing one takes a member list rather than the plain arrow shorthand.
Write the member form from memory — colon before the return type, properties beside it — and explain the intersection alternative plus the precedence trap when the parentheses are missing.
Reach for a hybrid type when modelling a real callable surface, and be able to say why producing a conforming value needs Object.assign or property assignment rather than an assertion that silences the check.
Weigh the API shape itself: a callable-with-properties reads nicely but is harder to tree-shake, mock and document than a plain object of named functions, and that tradeoff is yours to set for a public package.
## Two ways to write one callable TypeScript has two syntaxes for "this can be called": ```ts type Shorthand = (message: string) => void; interface Member { (message: string): void; } ``` As *callables* these are the same type — `Shorthand` and `Member` are mutually assignable. The difference is structural: the shorthand is a self-contained type expression, while the member form lives inside a member list that can hold other members. ## The hybrid type That second property is the whole reason the member form exists. JavaScript functions are objects, so a library can perfectly well export something you call *and* read properties off: ```ts interface Logger { (message: string): void; level: number; reset(): void; } declare const log: Logger; log('starting'); // uses the call signature log.level = 2; // uses the property log.reset(); // uses the method member ``` All three lines type-check against a single type. This is how you model a callable configuration object, a memoised function that exposes `.cache`, a `parse` function that also offers `parse.strict(...)`, or a middleware factory that carries defaults. ## Why the shorthand cannot do it In a function type expression the return type extends as far to the right as the parser can take it. So the natural-looking attempt is silently a different type: ```ts type Wrong = (message: string) => void & { level: number }; // parses as: (message: string) => (void & { level: number }) ``` The property landed inside the *return type*, not on the function. To attach it to the callable you must parenthesise: ```ts type Right = ((message: string) => void) & { level: number }; ``` That intersection is equivalent to the member form for practical purposes, and it is what people usually write when the callable part is already a named type. Getting this precedence wrong is one of the most common self-inflicted TypeScript bugs — the code compiles, and every property access on the value then fails to resolve. ## Interfaces are not special here A frequent misconception is that only interfaces can be callable. Object type literals accept the same member syntax: ```ts type Logger = { (message: string): void; level: number; }; ``` The interface version differs in the ways interfaces always differ (they participate in declaration merging, they can be `extends`-ed by other interfaces), not in callability. ## Call signatures alongside other member kinds A member list can mix a call signature with ordinary properties, methods, optional members, `readonly` members and index signatures: ```ts interface Route { (path: string): string; readonly base: string; strict?: boolean; } ``` What you should *not* do is treat a hybrid type as a place to describe several different ways of calling the same function — a member list holding two or more call signatures is an overload list, which is a separate topic with its own resolution rules. A hybrid type is about the callable plus its data. ## Nothing exists at runtime The type is a description, not a constructor. Declaring `Logger` emits nothing; it does not create a function, does not attach `level` to anything, and does not verify at runtime that the property is there. That has a practical consequence: producing a value of a hybrid type takes real work in value space — typically `Object.assign(fn, { ...props })`, or assigning properties onto a function declaration so the compiler folds them into its inferred type. A bare arrow function will not satisfy `Logger`, and a type assertion that claims it does is a promise the compiler simply believes. ## Reading the type back out Once you have a hybrid value, `typeof value` in type position gives you the whole thing — callable plus properties — which is the cheapest way to keep a wrapper or a test double in sync with it. ## Interview framing Say the mechanism, not the vocabulary: "a call signature written as a member lets the same type also declare properties, which is how you type a function that carries state". Then note the precedence trap in the intersection spelling, and finish with the erasure point — the type describes the shape, and building a value that actually has that shape is a separate job.
- Can a type alias declare a call signature, or is that interface-only?A type alias works fine: `type Logger = { (m: string): void; level: number }` uses the same member syntax. Callability is a property of the object type, not of the `interface` keyword. Interfaces differ in other respects — they merge across declarations and can be extended — but not in whether they can describe something callable.
- How would you type `parse('x')` and `parse.strict('x')` on the same value?One hybrid type: a call signature for `parse` itself, plus a property whose type is a function type. `interface Parse { (input: string): Config; strict: (input: string) => Config }`. The nested helper is nothing special — it is an ordinary property that happens to hold a callable, so it can be written with the arrow shorthand.
- Does declaring a hybrid type cost anything at runtime?Nothing. The declaration is erased at compile time — no object is created, no property is attached, and no check verifies that a value claiming the type actually has the property. Building a conforming value is a separate job in value space, usually `Object.assign` or assigning properties onto a function declaration.
saying these in an interview costs you the question
- Thinks only interfaces can be callable, not object type aliases
- Writes `(m: string) => void & { level: number }` and expects the property on the function
- Says a function cannot have properties because it is not an object
- Believes the hybrid type creates or attaches the properties
- Confuses a call signature with a construct signature that needs `new`