DEV Community

Cover image for TypeScript Tips That Actually Matter in Real Projects (including the satisfies operator)

TypeScript Tips That Actually Matter in Real Projects (including the satisfies operator)

Gavin Cettolo on June 24, 2026

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...
Collapse
 
gavincettolo profile image
Gavin Cettolo • Edited

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.

Collapse
 
nyaomaru profile image
nyaomaru

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 like isString, 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

Collapse
 
gavincettolo profile image
Gavin Cettolo

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.

Collapse
 
lucaferri profile image
Luca Ferri

This is an important comment. I was exactly thinking to these points

Collapse
 
paolozero profile image
Paolo Zero

You anticipated me 😂

Collapse
 
nazar-boyko profile image
Nazar Boyko

Quick one on Tip 7. When you derive a type with ReturnType<typeof getUser>, doesn't role come 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. The satisfies section was the clearest write-up of that operator I've read, by the way.

Collapse
 
gavincettolo profile image
Gavin Cettolo

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 ReturnType works 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 satisfies section, I'm glad you found it useful!

Collapse
 
junhao profile image
Ahmed bahar

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.

Collapse
 
gavincettolo profile image
Gavin Cettolo

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, and unknown only 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.

Collapse
 
elenchen profile image
Elen Chen

You got so many points 😀
Thank you for this clear and easy list

Collapse
 
gavincettolo profile image
Gavin Cettolo

Thank you @elenchen

Collapse
 
frank_signorini profile image
Frank

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.

Collapse
 
gavincettolo profile image
Gavin Cettolo

Good question, this comes up quite often when people start using satisfies.

In practice, satisfies has 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 satisfies itself 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?

Collapse
 
mudassirworks profile image
Mudassir Khan

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.

Collapse
 
vishdevwork profile image
Vishwajeet Kondi

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.