Lesson 60 of 80
tsc checks that your types line up. It has nothing to say about a forgotten await or a missing switch case with no default branch — that's what a handful of specific typescript-eslint rules exist to catch instead.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
tsc answers one question: do the types agree? It has no opinion on a promise you created and never awaited — that's a perfectly well-typed Promise<void> sitting unused, not a type error. A handful of typescript-eslint rules exist specifically to catch the bugs that are type-correct and still wrong. They're not style rules about semicolons or quote marks; they're a second, narrower kind of check that needs actual type information to run, layered on top of what tsc already does.
async function logAnalyticsEvent(name: string): Promise<void> {
await fetch("/api/track", { method: "POST", body: name });
}
function onButtonClick() {
logAnalyticsEvent("click");
// ❌ @typescript-eslint/no-floating-promises
// the promise is created and never awaited, .then()'d, or .catch()'d —
// if the fetch rejects, it becomes an unhandled rejection nobody sees
}
// ❌ @typescript-eslint/no-misused-promises
button.addEventListener("click", async () => {
await logAnalyticsEvent("click");
// addEventListener's handler type expects void, not Promise<void> — an async
// handler here still returns a promise the listener silently discards, so a
// rejection inside it never reaches anything that could handle it
});
Both examples compile cleanly under plain tsc — an unhandled Promise<void> and an async callback passed where a void-returning one is expected are both individually well-typed. no-floating-promises flags the first: a promise-returning expression used as a bare statement, with no await/.then/.catch in sight. no-misused-promises flags the second: a promise-returning function passed somewhere the surrounding API contract expects a plain void-returning one.
logAnalyticsEvent returns a Promise, which needs the TypeScript type checker wired into ESLint via parserOptions.project — enabling @typescript-eslint/recommended alone, without the recommendedTypeChecked config, leaves both rules silently inert.never trick. The switch-exhaustiveness-check rule catches a missing case even in a switch with no default clause at all — the manual const _exhaustive: never = action pattern from the never lesson only fires inside a default branch that has to exist for it to live in.tsc uses — a large project sometimes runs it as a separate, less-frequent CI step rather than on every keystroke in the editor.no-floating-promises is about a promise nobody ever touches again after creating it; no-misused-promises is about a promise-returning function fitting into a slot that expects a synchronous one. Confusing them means reaching for the wrong fix (adding await doesn't help a callback-signature mismatch).no-floating-promises and no-misused-promises are lint-level checks on top of the async/await and promise mechanics from the JavaScript track — they don't change how promises behave, they just refuse to let a well-typed-but-dangerous use of one pass silently. The exhaustiveness rule sits directly next to the manual never-based trick from earlier in this track, as a stricter, automatic version of the same underlying idea: make an unhandled case a build failure instead of a runtime gap.