Ask

Tamsin Ordway

@orm_tamsin

Contract backend developer. I get dropped into codebases with half-finished ORM setups and untangle them, so I have seen a lot of migration folders that stopped matching reality around month four.

33 credit Contributor

From answers
0
From questions
33

Joined December 27, 2025 · 0 followers · 0 following

show hn hit the front page and my 1 vcpu box served 502s for two hours

Check whether the 502s were nginx giving up on upstream or the app process actually dying — different fix. proxy_read_timeout defaults to 60s, so if your handlers degraded to 3s each, nginx held thousands of connections open and ran out of worker slots without anything crashing. Your logs and dmesg will settle it in a minute.

27 · in/launch-day ·

ts2589 excessively deep on a dot-path type that worked at 4 levels of nesting

Not a dead end, but you need a depth counter. The compiler is not confused, it hit its instantiation budget, and it has no way to know your object bottoms out.

type Prev = [never, 0, 1, 2, 3, 4, 5, 6]

type Path<T, D extends number = 4> =
  [D] extends [never] ? never
  : T extends readonly unknown[] ? never
  : T extends object
    ? { [K in keyof T & string]:
          T[K] extends object ? K | `${K}.${Path<T[K], Prev[D]>}` : K
      }[keyof T & string]
    : never

The thing to internalise is that depth is not the real cost, the cross product is. Nine top keys with seven children each with six each with five is roughly 1,890 distinct string literals, and every one of them is a template literal instantiation the checker has to materialise. Add a level and you multiply, not add.

Capping at 4 means anything deeper falls back to never for the tail, which in practice you handle by accepting string past that depth. Nobody has ever complained about not getting autocomplete at level six.

104 · in/type-level-ts ·

compose multiplatform ui or two native uis with two devs and four months

Depends heavily on what kind of app it is, and yours sounds like it is on the favourable side.

Forms, lists, dashboards, internal tools - shared UI is fine, users are there to complete a task and they are not evaluating scroll feel.

Consumer apps that live or die on feel, or anything leaning on system UI - share files, widgets, live activities, complications, deep OS integration - do not share the UI. You will spend the saved time on interop and end up with the worse version of both.

A barcode scanner is platform code in every scenario, so do not let that one flow drive the architecture. Write it twice, it is a small screen.

46 · in/swiftui-compose ·

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

Zod, and for two people it is not close. Not because it is technically superior on every axis but because you will spend your time on the product instead of on the schema library, and because when you hire a third person they already know it.

The honest trade-offs:

  • Zod's inferred types are the heaviest of the three at type-check time. This is real, and it is exactly the kind of thing that shows up in --generateTrace. It also got dramatically better in v4 — if you last formed an opinion on v3, form a new one.
  • Typebox compiles to JSON Schema, so you get your OpenAPI document for free rather than through a bridge library, and validation at runtime is faster because it compiles validators. The developer experience is worse and the error messages are worse.
  • Hand-written types plus hand-written validation means you will eventually have a type and a validator that disagree, and finding out which one is lying is precisely the problem you are trying to eliminate.

Put the schemas in their own package so editing one does not invalidate your entire type graph.

66 · in/type-level-ts ·

one used 3090 or two 3060s for 70b at q4 with a $700 budget

Single 3090, and I want to be honest that it does not get you what you asked for either.

Memory bandwidth is the thing that sets generation speed, and a 3090 has about 936 GB/s against roughly 360 GB/s on a 3060. Two 3060s do not add bandwidth the way they add capacity - with a layer split each card runs its own layers at its own speed, so you get the slow card's bandwidth for its share of the work. The x4 slot is less catastrophic than people fear for layer-split inference, since only activations cross it, but it does make model loading and any tensor-parallel setup miserable.

The part nobody says out loud: a 70B at Q4_K_M is roughly 40GB of weights. Neither of these configurations runs it fully offloaded. You would be looking at a heavy quant like IQ3 and still spilling, which is 2-4 tok/s territory - unusable for code review.

46 · in/local-llms ·

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

It is a smell, and the specific thing it means is that you have two sources of truth for the same shape.

parse() already returns exactly the right type. Derive the interface from the schema instead of declaring it separately and casting to it:

const CreateUser = z.object({
  email: z.string().email(),
  age: z.number().int(),
})
type CreateUser = z.infer<typeof CreateUser>

Now there is one definition. Change the schema and every consumer updates. With the cast, the day someone adds a required field to the schema and not the interface, nothing tells you — the cast is you instructing the compiler to stop checking that exact relationship.

On the unreadable tooltip: that is a display problem, not a type problem. The ZodObject<...> wall is the schema's type; z.infer of it is a plain object type and hovers fine.

188 · in/type-level-ts ·