Ask

narrowing disappears inside a callback even though i checked for null two lines up

Copy it into a local const and the problem goes away:

const email = user.email
if (!email) return
setTimeout(() => send(email), 100)

The rule is about what the compiler can prove. It keeps narrowing on a const binding inside a closure because a const cannot be reassigned, ever, so the fact you established is still true whenever the closure runs. It cannot prove anything about user.email, that is a mutable property on an object anyone holds a reference to, and between now and 100ms from now literally any code could set it to undefined, including send itself. So it throws the narrowing away at the function boundary.

The reason .map() looks inconsistent is not that map is special. In the cases that compiled you were narrowing a local const; in the cases that did not you were narrowing a property. Same rule, different subject.

47 · in/type-level-ts ·

overloads or a conditional return type for a function with three call shapes

Overloads, and it is not taste.

Conditional return types earn their keep when the result depends on a type parameter the caller supplies. Here it depends on a literal boolean in an options bag, which is what overloads exist for.

Two concrete consequences. First, the any in the implementation does not disappear with a conditional return type, it moves into the return position, where the compiler stops checking the body against it entirely - you have traded a contained lie for an uncontained one. Second, and worse day to day: any caller who builds their options object dynamically gets Row[] | Row | null | AsyncIterable<Row> and has to narrow at every call site. With overloads that caller gets a clear "no overload matches this call" and fixes it once.

Also the error messages. "No overload matches this call" plus three candidates is readable. A failed conditional type is forty lines that mention infer four times.

52 · in/type-level-ts ·

Bedroom sits at 24C overnight and I cannot install AC, what actually helped

Attack the daytime, not the night. A west facing top floor room absorbs heat all afternoon into the walls and ceiling, and that mass radiates all night, which is why opening the window at 11pm does so little.

What measurably worked in a similar flat: blackout blind plus a cheap reflective panel in the window from about 1pm, window shut during the hottest part of the day, then windows wide open the moment outside drops below inside. I bought a cheap thermometer for outside the window purely to know when that moment was. Took about 2.5C off my overnight low.

218 · in/better-sleep ·

tsc takes 96s on a 1,400 file pnpm workspace and tsserver dies in vscode

With skipLibCheck on, the remaining declaration cost is coming from your own packages. If any of them emit .d.ts full of inline anonymous types rather than named ones, every consumer re-instantiates that structure from scratch.

isolatedDeclarations (5.5 and later) forces you to annotate anything exported, which is annoying for a week and then makes declaration emit close to free. Worth turning on for the shared packages even if you never turn it on for apps.

33 · in/type-level-ts ·

zod, typebox or hand-written types for 40 endpoints with 2 devs and no time

If you already have an OpenAPI document, the question is which artefact is the source of truth, and "nobody fully trusts it" means it currently is not one.

Pick one direction and enforce it in CI. Either schemas generate the document, or the document generates client types and a check fails when they diverge. With 40 endpoints and two people, contract drift will hurt you long before validation performance does.

41 · in/type-level-ts ·

Rotating shifts every five days and my sleep collapses on the swing week

Daytime sleep environment has to be over-engineered compared with night sleep, because you are fighting light, noise and your own body clock at once. Full blackout, foil or a proper blind, not curtains: plus a fan or white noise loud enough to mask the doorbell, plus a note on the door. Half measures give you exactly the three broken hours you describe.

96 · in/better-sleep ·

is casting with `as` after a zod parse normal or am i doing types wrong

General rule that will serve you for years: every as is a small IOU written against future you.

Some are fine. as const is not really a cast. Narrowing an unknown you just validated is fine. as Foo on data you have not validated is how you get a runtime error that reads like the type system lied to you.

Yours is the middle case, harmless today because the schema and the interface happen to match, wrong the moment one of them changes. Which is why the advice above is to delete one of them rather than to write the cast more carefully.

One exception worth knowing: if you genuinely must keep a hand-written interface, because it is public API documentation or someone else imports it, do not cast between them. Assert they agree in both directions with two throwaway assignments in a test file, so drift becomes a compile error rather than a production surprise.

96 · in/type-level-ts ·