TypeScript Beyond the Basics: Generics, Utility Types and Strict Mode in Real Projects

Most teams use TypeScript as a linter and stop there. Generics that preserve your domain types, a short set of utility types, and the two compiler flags people skip are what turn the compiler into a refactoring tool.

۶ مهر ۱۴۰۵

3 min read

TypeScript Beyond the Basics: Generics, Utility Types and Strict Mode in Real Projects post featured image

TypeScript Beyond the Basics: Generics, Utility Types and Strict Mode in Real Projects

Most teams adopt TypeScript for autocomplete and to stop shipping the classic string versus number mistakes. Six months later the codebase is full of any casts and a compiler config nobody dares to touch. That gap is vocabulary, not effort.

Generics that earn their keep

A generic earns its place when one piece of logic must serve several shapes without any of them losing type information. If you end the implementation with a cast, you needed a plain function.

  • A repository that takes the entity type once and returns it from every method.
  • A form builder where each field declares its value type and the result is inferred.
  • A table component taking a row type plus accessors, so sorting stays correct per column.
  • A typed event emitter whose map defines payloads and legal event names.

Two constraints do the heavy lifting, since keyof T accepts only keys the type really has and extends string keeps an argument inside a known family.

The utility types you actually need

Nobody memorises a hundred of these. A short set covers almost everything, and composing them in one named alias beats inventing a type per model.

  • Partial for update payloads and form models.
  • Pick and Omit to build request DTOs without repeating a field name.
  • ReturnType and Parameters so a hook documents its own shape once.
  • NonNullable for values that come back from a nullable column.
  • Record for a typed dictionary instead of a permissive index signature.
  • Awaited to unwrap promises returned by clients that return promises.

The two compiler flags people skip

Strict mode is the baseline. The upgrade almost nobody enables is noUncheckedIndexedAccess, and it catches the most real bugs, because reading outside a known range is the commonest source of runtime undefined.

  • Array and record access becomes value or undefined, so out-of-range reads fail to compile.
  • Strict null checks force you to handle the missing case instead of asserting it away.
  • exactOptionalPropertyTypes blocks assigning undefined to an optional field.
  • noImplicitOverride and the switch fallthrough flag clean up inheritance mistakes.

Turning them on for an old codebase produces a burst of errors, so enable them for new modules and fix files as you touch them.

Discriminated unions beat optional fields

An object with three booleans and four nullable fields has eight meaningful states and the compiler checks none of them. A union with a literal status makes the invalid combinations type errors.

  • Model a payment as created, captured or refunded, not a status string with nullable fields.
  • Model a form flow as a union of step objects, each carrying only its own data.
  • Narrow with a switch on the discriminant and the union narrows inside each branch.

Where to stop

Type-level programming has a ceiling, and crossing it is the fastest way to make a team distrust its own types.

  • Do not build a type that needs a comment longer than the code it replaces.
  • Do not hide one implementation behind a generic parameter until a second one exists.
  • Do not encode business rules in types, encode states and relationships.
  • Parse external data once at the boundary, then use that type everywhere else.

Key takeaways

  • Add a generic only when shapes share behaviour and stay distinguishable.
  • Keep a short set of utility types and compose them.
  • Turn on noUncheckedIndexedAccess first, then the other strict flags file by file.
  • Replace clusters of nullable fields with discriminated unions.
  • Stop before the types get harder to read than the logic.

FAQ

Q: Does heavy type level code slow the compiler down on a large codebase?

A: Sometimes, and the cost lands on the largest files, not the whole project. Deep mapped types and generics instantiated with large unions are the usual culprits, so simplify a couple of aliases rather than removing the flags.

Q: Should I enable strict mode on an existing codebase at once?

A: Not at once. Keep the flags out of the global build for legacy folders, apply them to new modules immediately, and fix errors in small batches as you touch each file.

socials-vertical icons

142

socials-vertical icons

2180

you might also like...

Moving from WordPress to Next.js: a migration guide for Persian websites post featured imageMoving from WordPress to Next.js: a migration guide for Persian websites

The staged path from a slow WordPress site to a fast Next.js one, keeping Persian content, Persian URLs and search traffic intact.

Next.js App Router: Why Server Components Changed How I Build Websites post featured imageNext.js App Router: Why Server Components Changed How I Build Websites

The App Router and React Server Components are not just new APIs; they change where your code runs. Here is how they cut bundle size, simplified data fetching, and made my pages measurably faster.

parsaaghayi's blog logoparsaaghayi's blog logo

© 2024

All Rights Reserved , Inc.

parsa aghayi
TypeScript Beyond the Basics: Generics, Utility Types and Strict Mode in Real Projects | Parsa Aghayi's Blog