3 ms·
I recently came to a production Typescript codebase and it took minutes to compile. Strangely, it could not behave correctly without a linter rule `no-floating-
by renewiltord 1y ago
I recently came to a production Typescript codebase and it took minutes to compile. Strangely, it could not behave correctly without a linter rule `no-floating-promises` but the linter also took minutes to lint the codebase. It was an astounding exercise in patience. Faster linters like oxlint exist but they don't have a notion of cross-file types so `no-floating-promises` is impossible on them.
The worst part is that `no-floating-promises` is strange. Without it, Knex (some ORM toolkit in this codebase) can crash (segfault equivalent) the entire runtime on a codebase that compiles. With it, Knex's query builders will fail the lint.
It was confusing. The type system was sophisticated enough that I could generate a CamelCaseToSnakeCase<T> type but somehow too weak to ensure object borrow semantics. Programmers on the codebase would frequently forget to use `await` on something causing a later hidden crash until I added the `no-floating-promises` lint, at which point they had to suppress it on all their query builders.
One could argue that they should just have been writing SQL queries and I did, but it didn't take. So the entire experience was fairly nightmarish.
- WorldMaker 1y agoPromise<T> is not T and Typescript's type system will catch that in most cases with a simple type assertion (add a type to the function return; add a `satisfies` check somewhere). If it isn't catching it often enough, without a lot more extra type assertions, you may be missing a Typescript strict flag like noImplicitAny. (In general I find that I prefer the full `"strict": true` in the compilerOptions of tsconfig.json and always write for all strict checks.) Also if your codebase is still relying on "explicit" `any`, try `unknown`. Also, yeah Knex doesn't look like the best ORM for a Typescript codebase. Typescript support in Knex is clearly an after thought, and the documentation admits it: > However it is to be noted that TypeScript support is currently best-effort.
- renewiltord 1y agoYeah, the codebase in question was using an “insert into db but do not check result and let exceptions inform” pattern. It was probably transformed from JS at some point which is why it used Knex I must guess. So I suppose I should have found some condition to force use of return values and then unleashed Claude code to fix up linting errors. It’s rarely a GetX resulting in a Promise<T> that’s mismatched and more an insertX followed by an insertY on the same transaction without awaiting the first insertX that’s the problem. Makes it a heisenbug. I wouldn’t mind it if the linter was oxlint speed but eslint was a 5 minute affair on the codebase. It’s sort of a Moravec’s Paradox of its own: web development is much more complex than the HFT strats we deployed (which could compile and sim something faster than this can lint). I suppose I can’t blame the query builder on the tool choice but they were pretty stuck on Knex by then. Your advice is appreciated. Thank you.
- WorldMaker 1y agoYeah, that's about the point where I would have started mandating every insert be awaited and not just "fire and forget" (or "let the unhandled promise rejection handler sort it out"). Take the linter's advice there and do it everywhere. Would solve a lot more timing heisenbugs and race condition uncertainty, in my experience. Also yes, eslint is quite slow on larger codebases, but getting faster. I've more and more using `deno lint` which is similar to oxlint (also Rust based, also trying to be more or less similar to eslint with certain "presets" available) because I mostly like its typescript "required" opinions (I don't configure it much beyond its out of the box configuration). You can use `deno lint` just fine even if you aren't using deno as a runtime. I think if I was working on a production Node app with a large codebase right now I'd consider `deno lint` or `oxide` as first pass in the inner developer loop and `eslint` configured with the same presets as a CI-mostly second pass.
- mikojan 1y agoYou don't have to suppress it, you just write `void` in front of the floating promise. Which is arguably the correct way to handle this situation anyway. It makes it unmistakably clear that you are intentionally throwing away a return value.