skip to content

In TypeScript, calling this helper reports "Assertions require every name in the call target to be declared with an explicit type annotation": `const assertIsString = (v: unknown): asserts v is string => { if (typeof v !== "string") throw new TypeError(); };`. Why is the call rejected, and how do you fix it?

level: middleimportance: should knowfreq 42%

answer

  1. the declaration is fine, the call is not
  2. checker will not infer the callee
  3. every name in the call path
  4. function declaration or annotated binding
  5. dotted target needs a typed container

basics

~20 s

An assertion call only works when every name in the call target has a written-out type, and this const's type is inferred from its arrow initializer. Fix it by annotating the binding with a function type, or by using a function declaration.

solid answer

~50 s

The checker has to know the callee's signature before it can apply the call's effect on control flow, so it refuses to work from a type it would first have to infer. A `const` initialised with an arrow function has exactly that — an inferred type — so the compiler reports TS2775 at the call site. Note where the error lands: the declaration is legal, only the call is rejected. There are two fixes. Write it as a function declaration, `function assertIsString(v: unknown): asserts v is string { ... }`, which carries its own explicit signature. Or keep the arrow and annotate the binding: `const assertIsString: (v: unknown) => asserts v is string = (v) => { ... }`. The rule covers dotted targets too, so `guards.assertIsString(x)` also needs `guards` itself to have an explicit type annotation.

code

typescript · 19 lines
typescript
// rejected at the call site: the const's type is inferred
const inferredGuard = (v: unknown): asserts v is string => {
  if (typeof v !== "string") throw new TypeError("not a string");
};

// fix 1 - a function declaration carries its own signature
function declaredGuard(v: unknown): asserts v is string {
  if (typeof v !== "string") throw new TypeError("not a string");
}

// fix 2 - annotate the binding, contextually type the arrow
const annotatedGuard: (v: unknown) => asserts v is string = (v) => {
  if (typeof v !== "string") throw new TypeError("not a string");
};

declare const raw: unknown;
// inferredGuard(raw);  // TS2775
declaredGuard(raw);
annotatedGuard(raw);

go deeper

for a junior

Recognise the error text and the one-line fix: declare the helper with the function keyword instead of assigning an arrow function to a const.

for a middle

Explain that control-flow analysis needs the callee's declared signature and will not infer one, and give both fixes — a function declaration or an explicitly typed binding.

for a senior

Extend the rule to dotted call targets and cross-module imports, and note that the defining module owns the fix because consumers cannot repair it at their call sites.

for a principal

Decide the codebase convention that avoids the trap by construction — for example, guard helpers exported as function declarations — so no reviewer has to rediscover TS2775.

## The error, precisely ```ts const assertIsString = (v: unknown): asserts v is string => { if (typeof v !== "string") throw new TypeError("not a string"); }; declare const raw: unknown; assertIsString(raw); // error TS2775: Assertions require every name in the call target // to be declared with an explicit type annotation. ``` Two details catch people out. First, the *declaration* is fine — writing an assertion return type on an arrow function is legal, and the compiler says nothing about it. The complaint appears at the call site, which is why the error often looks like it is about the wrong line. Second, the message says "every name in the call target", not "the function" — that wording is doing real work, as the qualified-name case below shows. ## Why the rule exists An assertion call is not an ordinary call. It participates in control-flow analysis: the checker must decide what the code *after* the call knows. To do that it needs the callee's signature at the moment it walks the flow graph. If the callee is an un-annotated `const`, its type is not written down anywhere — it has to be inferred from the initializer, and that inference can itself depend on flow analysis. Rather than open a circularity, TypeScript declines and asks you to state the type. The practical shape of the rule: the compiler will follow a name to a *declared* type, but it will not run inference to obtain one. ## The two fixes A function declaration is the simplest answer. Its signature is the declaration; nothing needs inferring. ```ts function assertIsString(v: unknown): asserts v is string { if (typeof v !== "string") throw new TypeError("not a string"); } ``` If you prefer arrow functions, annotate the binding with a function type whose return type is the assertion signature, and let the initializer's parameters be contextually typed: ```ts const assertIsString: (v: unknown) => asserts v is string = (v) => { if (typeof v !== "string") throw new TypeError("not a string"); }; ``` Both forms narrow correctly at every call site. The second is noticeably more verbose, which is one reason assertion helpers are conventionally written as function declarations. ## Qualified names and the object-literal trap Because the requirement covers every name in the call path, grouping guards in an object hits the same wall: ```ts const guards = { assertIsString(v: unknown): asserts v is string { if (typeof v !== "string") throw new TypeError("not a string"); }, }; guards.assertIsString(raw); // TS2775 again ``` The method carries an explicit signature, but `guards` does not — its type came from inference over the object literal. Annotating the container fixes it: ```ts type Guards = { assertIsString(v: unknown): asserts v is string }; const guards: Guards = { assertIsString(v: unknown): asserts v is string { if (typeof v !== "string") throw new TypeError("not a string"); }, }; guards.assertIsString(raw); // narrows ``` There is a companion restriction on the *form* of the call target. It must be an identifier or a qualified (dotted) name — nothing computed. `guards["assertIsString"](raw)` is rejected with TS2776, "Assertions require the call target to be an identifier or qualified name". The same applies to a callee produced by a conditional expression or returned from another call: there is no name for the checker to resolve, so no assertion effect is applied. ## Imports behave the way you would hope — mostly Importing a helper does not weaken the rule; it just depends on how the helper was written in the other module. An imported `function` declaration works, because its signature is declared. An imported `const` holding an un-annotated arrow function fails at the call site with the same TS2775, in the importing file. So the fix belongs in the module that defines the guard, and getting it wrong there breaks every consumer. ## How this shows up in real code The symptom is usually not the error at all — teams that write assertion helpers as un-annotated arrow consts see the red squiggle immediately. The subtler failure is a codebase that standardises on arrow-function exports for stylistic reasons and then finds its guard helpers uniquely awkward. Knowing the rule turns a puzzling error into a one-line decision: use `function` for assertion helpers, or pay for the explicit function-type annotation.

  • Why does the error appear at the call site rather than on the declaration?
    Because the declaration is legal — an arrow function may carry an assertion return type. The restriction is about applying the call's control-flow effect: at that moment the checker needs a declared signature for the callee and will not infer one, so it reports TS2775 where the assertion would have been used.
  • Does grouping assertion helpers on an object still work?
    Only if the object binding itself has an explicit type. `const guards = { assertIsString(...) {...} }` fails, because `guards` has an inferred type; declaring `const guards: Guards = {...}` fixes every call through it. And the target must be a dotted name — `guards["assertIsString"](x)` is rejected with TS2776.
  • If the helper lives in another module, whose job is the fix?
    The defining module's. An imported `function` declaration works everywhere; an imported un-annotated arrow const fails at each call site in the consuming files. Since the importer cannot repair it locally, guard helpers are usually exported as function declarations so no consumer has to think about it.

saying these in an interview costs you the question

  • Blames strict mode or a missing tsconfig flag
  • Adds an `as` cast at the call site to silence it
  • Says the arrow function syntax cannot carry asserts
  • Thinks annotating only the parameter is enough
  • Assumes importing the helper sidesteps the rule

context