explicit-return-type
an exported function with no declared return type
The argument
The bright line is the export keyword — the same trigger as "own file" and the Params interface:
- Exported function → declare the return type. The annotation is the output half of the public contract, exactly as
Paramsis the input half. - Non-exported function (private helpers, callbacks) → always infer. Annotations on internals are noise; the consumer is in the same file and inference is precise there.
Why this rule exists: with inference, an exported function's return type is whatever the body happens to return today. A refactor can silently widen or change the public contract, and the diff reads as an implementation edit — the error surfaces later, in a consumer's file, several inference hops away. An explicit annotation fails at the definition site the moment the body stops satisfying the contract, and an intentional API change becomes a visible diff line. It also keeps the codebase compatible with TypeScript's isolatedDeclarations.
✅ GOOD:
interface Params {
user: User | null;
}
export const getUserDisplayName = ({ user }: Params): string => {
// ...
};
const sumTotals = ({ records }: { records: ReportRecord[] }) => {
// private helper — inferred
};
❌ BAD:
export const getUserDisplayName = ({ user }: Params) => { /* ... */ }; // WRONG — exported, contract is implicit
const sumTotals = ({ records }: { records: ReportRecord[] }): number => { /* ... */ }; // WRONG — internal, annotation is noise
Exceptions (inference is correct on these even when exported):
- Generic-heavy signatures — when the written return type would be an unreadable conditional-type expression, the generic signature is the contract; infer.
- Interface-pinned signatures — methods implementing a declared interface (e.g., a
RecordSourceimplementation) are already contracted by the interface; restating the type is duplication.
(Framework-specific exceptions live with their frameworks: the React document exempts components, and the TanStack Start document exempts query-options factories — each loads only for repos using that framework.)
A non-exported function keeps an annotation when removing it changes what the compiler accepts — contextual typing of literals (an array of tuples inferring as a wider union), a recursive helper, excess-property checking on an object literal. The bright line stays: annotate exports; do not annotate internals for documentation.
Migration: new exported functions comply immediately; existing exported functions gain a return type when touched. Never remove a return type from an exported function.
The proof
interface Params {
user: { name: string | null } | null;
}
// Exported with an implicit contract: whatever the body happens to return today
// is what consumers are compiled against.
export const getUserDisplayName = ({ user }: Params) => user?.name ?? 'Unknown';
// JavaScript, where a return type annotation is not syntax that exists. The
// rule has nothing to ask of this file — asking anyway produced blocking
// findings no agent or human could ever resolve.
export const formatInitials = ({ name }) =>
name
.split(' ')
.map((part) => part[0])
.join('');
Turn it down
Both lines go in your lightsout.config.json.
"standards-checks": { "explicit-return-type": "advisory" }"standards-checks": { "explicit-return-type": "off" }