Adopting TypeScript is step one. Using it well is where the real benefits appear. If you are still weighing the switch, start with TypeScript vs JavaScript. Already using it? We combined the official TypeScript documentation, the typescript-eslint project and two practitioner checklists into ten habits that consistently show up.
Key takeaways
- Turn on
strict, avoidany, and validate external data at runtime. - Model state with discriminated unions and let the compiler check you have handled every case.
- Let inference do the easy work, and enforce the rules automatically in CI.
1. Turn On Strict Mode
The TypeScript reference explains that strict enables a family of checks at once: noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, useUnknownInCatchVariables and more. It is marked as recommended for new projects, with one caveat: future TypeScript versions may add new checks under strict, so upgrades can surface new errors. Practitioner lists add two optional flags worth considering: noUncheckedIndexedAccess (indexed reads include undefined) and exactOptionalPropertyTypes, which catches bugs strict mode alone misses.
2. Avoid any, Prefer unknown
typescript-eslint explains why: any is an escape hatch that disables most type checking for the values it touches, and because it spreads through code, other type-aware rules have nothing to check. When you truly do not know a type, use unknown, which forces you to narrow it before use. When you do know the shape, describe it with an interface or type; for several possibilities use a union; for arbitrary keys use an index signature. The same documentation allows any as a last resort, for instance while migrating or when a package lacks types, and recommends a targeted disable comment rather than turning the rule off.
3. Let Inference Work
The handbook's advice is to start with fewer annotations. TypeScript usually infers variable types from initial values and often infers return types too. Redundant annotations can widen types and add upkeep. Add explicit types at boundaries: function parameters, exported APIs and public return types.
4. Model State With Discriminated Unions
Instead of several optional fields or boolean flags, use a union with a literal tag, such as a request that is loading, error (with a message) or success (with data). Invalid combinations become impossible to write. Both practitioner lists recommend this over loose flags.
5. Check Exhaustively
In a switch over a union, add a default branch that assigns the value to type never. If someone adds a new member to the union later, the compiler flags every place that has not handled it.
6. Narrow Instead of Asserting
The handbook shows how typeof, Array.isArray and equality checks narrow a type inside a branch. A type assertion (as T) just tells the compiler to trust you and has no runtime effect, so use it sparingly. Both checklists also advise avoiding the non-null assertion (!) to skip checks. Prefer satisfies over as when you only want to check a value against a type while keeping its precise inferred type, and write type predicates (value is T) for checks that need to narrow at runtime.
7. Validate Data at the Edges
This is the biggest gap in static typing: TypeScript cannot validate external data. Casting response.json() with as User compiles but can still fail at runtime. API responses, form input, environment variables and JSON files need runtime checks. Validate at the boundary with a schema library such as Zod (or hand-written type guards), then let types carry the validated data inward. Infer the TypeScript type from the schema so the two never drift apart.
8. Use Utility Types and Derive Types From Values
Pick, Omit, Partial, Required and indexed access types derive new types from existing ones instead of duplicating them. Building a union from a runtime as const array keeps the list and the type in sync, and literal unions are often easier to refactor and serialise than enums.
9. Prefer Immutability and Keep Types Close
Mark data that should not change as readonly and use as const for fixed lists and configuration so values stay literal. Hyring's checklist also recommends colocating types with the code that uses them, rather than one giant "types" file, and keeping functions small and single-purpose so their types stay readable. Naming conventions help too: PascalCase for types, camelCase for values, and boolean names starting with is, has or should.
10. Enforce It Automatically
Run tsc --noEmit and a linter on every pull request, with rules that flag explicit any and floating promises. A type error should block a merge just like a failing test. This fits neatly into a workflow built with Git and GitHub.
Common Mistakes
- Sprinkling
// @ts-ignoreoras anyinstead of fixing the problem. - Over-engineering with complex generic types nobody can read.
- Trusting types for data that never passed validation.
- Adopting TypeScript without
strict, which keeps autocomplete but misses most of the safety.
Keep Improving
Great TypeScript is readable first and clever second. If you want an experienced team to set up a typed, tested and maintainable codebase, Node-devs can help you build it right. Our Node.js backend guide shows where these habits matter most.
References and further reading
This guide was compiled from the sources below. We compared them, kept the points they agree on and wrote the article in our own words. Check the originals for more detail.
- TSConfig reference: strict (TypeScript documentation)
- Everyday Types (TypeScript Handbook)
- no-explicit-any (typescript-eslint)
- TypeScript tips everyone should know (AllThingsSmitty (GitHub))
- TypeScript best practices (Hyring)




