Most TypeScript tutorials teach you the language.
This article teaches you how to use it.
There's a difference. The language has hundreds of feat...
For further actions, you may consider blocking this person and/or reporting abuse
A couple of clarifications before the comment section inevitably gets there 😄
Yes, Zod can be overkill for small projects. If you're validating a handful of inputs, manual type guards are perfectly fine. I included it because in larger codebases it tends to pay for itself quickly, especially when dealing with multiple external data sources.
Yes, generics can hurt readability when overused. My rule is simple: write the concrete version first. Only extract a generic once you notice you're repeating the same structure enough times to justify the abstraction.
As with most TypeScript features, the goal isn't to use them everywhere, it's to apply them where they reduce maintenance cost.
Thanks for the nice article! 😸
Totally agree with your clarification.
I think there’s also a nice middle-ground between fully manual guards and a schema library like Zod: composable type guards.
Start concrete with
isUser(value), but once patterns repeat, extract tiny guards likeisString,nullable,optional, and, or, etc., and compose them.That keeps the “write the concrete version first” mindset, while still reducing maintenance cost as the codebase grows.
This is the direction I’ve been exploring with
is-kit.github.com/nyaomaru/is-kit
Thanks! I completely agree that composable type guards are a very sensible middle ground.
My point in the article was mainly to encourage people to start with the simplest concrete guard that solves the problem, instead of reaching for a generic abstraction too early. But once you start seeing the same validation patterns repeated across the codebase, extracting reusable primitives like
isString,optional,nullable,and,or, etc. can definitely improve maintainability without introducing the full complexity of a schema library.I'll take a look at is-kit, it sounds like an interesting approach to keeping runtime validation lightweight while still benefiting from composition.
This is an important comment. I was exactly thinking to these points
You anticipated me 😂
Quick one on Tip 7. When you derive a type with
ReturnType<typeof getUser>, doesn'trolecome back as the literal'admin'rather than the'admin' | 'user'union you'd usually want? The implementation hardcodes'admin' as const, so the derived type ends up narrower than the real domain, and assigning a'user'would fail. I lean on deriving types from functions a lot, but that's the one spot where I still reach for an explicit return annotation so the type describes the contract instead of one sample value. Thesatisfiessection was the clearest write-up of that operator I've read, by the way.That's a great point, and it's one of the important caveats of deriving types from implementations.
In the article,
getUser()was intentionally simplified to illustrate the pattern, but in a real application you're absolutely right:ReturnType<typeof getUser>only reflects what TypeScript can infer from the implementation. If the implementation effectively returns a literal'admin', the derived type can become narrower than the actual domain model.In cases where the function represents a public contract rather than a specific implementation detail, I also tend to prefer an explicit return type annotation. That keeps the contract stable even if the implementation changes and avoids accidentally encoding a sample value into the type.
So I'd say
ReturnTypeworks best when the implementation is the source of truth, while explicit return annotations are often a better fit when the domain model is the source of truth.And thanks for the kind words about the
satisfiessection, I'm glad you found it useful!Hello Cettolo,
I hope you are doing well.
Excellent article.
This is the kind of TypeScript guidance that delivers real value because it focuses on patterns developers actually use in production rather than obscure type tricks.
The explanations around discriminated unions, satisfies, and unknown clearly demonstrate how strong typing can prevent entire classes of bugs before they reach users.
I particularly appreciate the emphasis on precision over complexity great TypeScript is not about writing clever types, but about accurately modeling business logic and maintaining long-term code quality.
Every frontend and full stack developer working with TypeScript can benefit from adopting these practices in their daily workflow.
Thanks a lot for the thoughtful feedback, I really appreciate it.
That framing is exactly what I was aiming for: focusing on patterns that hold up in real codebases rather than TypeScript “tricks” for their own sake. In practice, the value of things like discriminated unions,
satisfies, andunknownonly really shows when they’re used to model real domain constraints and reduce ambiguity at the boundaries.Glad the article resonated with that perspective, and thanks again for taking the time to write this.
You got so many points 😀
Thank you for this clear and easy list
Thank you @elenchen
I've been experimenting with the satisfies operator, but I'm curious about its performance impact - have you noticed any differences in larger projects? I'd love to swap ideas on this.
Good question, this comes up quite often when people start using
satisfies.In practice,
satisfieshas no runtime performance impact at all, even in large projects. It’s a purely type-level operator: TypeScript erases it completely during compilation, so the emitted JavaScript is identical to what you’d get without it.What can change (very slightly) in larger codebases is type-checking performance, but even there
satisfiesitself isn’t usually the culprit. The cost comes more from the complexity of the inferred types it enables you to express, not from the operator.So I tend to think of it as “free at runtime, neutral at compile time unless it leads to more advanced inference chains.”
Curious what kind of patterns you’ve been experimenting with: are you mainly using it for object literals, or also in more structural validation-like scenarios?
the unknown tip is the one that costs teams the most to learn late. we had any on every response.json() call for about 18 months. when we finally added schema validation at the boundary, we found three spots where the api was silently returning a different shape — the kind of bug that shows up as intermittent undefined on a field nobody touches until they do.
the satisfies + as const combo for route maps has become something we reach for constantly. specific type preservation means you catch typos in path params at compile time, not at runtime when a user hits a broken link.
any advice on migrating from any heavy code? we usually go layer by layer from the api boundary inward, but the order matters a lot.
The more TypeScript evolves, the more it feels like it's optimizing developer communication as much as compiler safety.
The compiler just happens to enforce it.