Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

For day-to-day TypeScript, the most useful patterns are the ones that make uncertainty and state changes explicit: check nullable values before using them, model distinct states with discriminated unions, handle every union case, and use generics when they preserve a real type relationship. Utility types can simplify common transformations, while assertions should be treated as promises to the compiler—not runtime validation.

These are practical patterns, not a ranking or a claim that every project should adopt them unchanged. The right choices depend on the codebase, especially when enabling stricter compiler checks in older projects.

1. Enable strict null checks when the project can support them

Without strict null checking, TypeScript may allow a value that could be null or undefined to be used as though it were definitely present. With strictNullChecks enabled, those possibilities are represented in the type system, so code must account for them before use. The TypeScript Handbook encourages enabling the option where possible, but an existing project may need substantial fixes before it can pass type checking.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For example, a lookup may not find a user:

function greeting(name: string | undefined) {
  return `Hello, ${name.toUpperCase()}`;
}

That function assumes name exists. A simple guard makes the assumption explicit:

function greeting(name: string | undefined) {
  if (name === undefined) {
    return "Hello, guest";
  }

  return `Hello, ${name.toUpperCase()}`;
}

In an older codebase, turn on the option as a planned migration rather than assuming the change is free. The compiler will surface places where nullable values need a guard, a fallback, or a better type.

The TypeScript Handbook’s basic types material discusses strict null checking.

2. Narrow values with ordinary control flow

When a value can have more than one type, check what it is before using operations that apply only to one possibility. TypeScript follows JavaScript control flow and narrows the type inside branches.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before, a union value cannot safely be treated as either a number or a string without checking:

function format(value: string | number) {
  return value.toFixed(2);
}

After, the branch makes the number-specific operation safe, while the other case remains a string:

Rank #2
TypeScript Programming Language - Software Engineer & Coder T-Shirt
  • TypeScript implements a superset of syntax for strictly typed development, facilitating deep static analysis and enhanced development environment integration. The compiler translates source into standard script formats, ensuring parity across any runtime.
  • TypeScript is ideal for front-end developers, full-stack engineers, and software architects who build large-scale web applications. It serves those looking to improve code excellence, reduce bugs through static checking, and maintain complex projects more.
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem
function format(value: string | number) {
  if (typeof value === "number") {
    return value.toFixed(2);
  }

  return value.trim();
}

The same idea applies to unknown input and optional object properties: use a real test, such as typeof, a null check, or a property check, before relying on a narrower type. A cast is not a substitute for that check.

The Handbook’s narrowing guide explains how checks and branches refine types.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Represent distinct states with discriminated unions

If a value can represent several different states, model those states directly instead of combining unrelated optional fields. Give each union member a shared property whose value is a distinct literal, then add the fields that belong to that state.

A loosely shaped response might permit impossible combinations:

type Result = {
  loading?: boolean;
  data?: string;
  error?: string;
};

A discriminated union ties each state to its relevant data:

type Result =
  | { state: "loading" }
  | { state: "success"; data: string }
  | { state: "failed"; error: string };

Now checking state tells TypeScript which member is in hand:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function message(result: Result) {
  if (result.state === "success") {
    return result.data;
  }

  if (result.state === "failed") {
    return result.error;
  }

  return "Loading";
}

In the success branch, data is available; in the failed branch, error is available. The representation also avoids implying that a result can simultaneously be loading, successful, and failed.

The narrowing guide and the unions and intersections guide describe discriminated unions.

4. Make union handling exhaustive

Once a union represents meaningful states, handle every state where a decision depends on it. A never check makes an omitted case a type-checking error when a new member is added.

type Result =
  | { state: "loading" }
  | { state: "success"; data: string }
  | { state: "failed"; error: string };

function describe(result: Result): string {
  switch (result.state) {
    case "loading":
      return "Loading";
    case "success":
      return result.data;
    case "failed":
      return result.error;
    default: {
      const unreachable: never = result;
      return unreachable;
    }
  }
}

If a new state, such as cancelled, is added to Result but not to this switch, assigning result to never no longer type-checks. That points to the handler that needs updating.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This catches missing branches against the declared type; it does not establish that data arriving from a network or another runtime boundary actually matches that type. The Handbook’s narrowing guide documents never for exhaustive checks.

5. Use generics to preserve useful type relationships

A generic is useful when a function works across several types but should preserve a relationship between its input and output. It is not a reason to make every function generic.

A non-generic identity helper can lose the specific type:

function identity(value: any): any {
  return value;
}

A generic keeps the caller’s type through the function:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function identity<T>(value: T): T {
  return value;
}

const count = identity(3); // inferred as number

The important detail is that the same type parameter appears in both positions: the return type reflects the type passed in. If a helper has no meaningful relationship to preserve, a simpler concrete type may be easier to read. The TypeScript Handbook’s generics guide covers reusable components that operate over varied types.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

6. Reach for utility types for common transformations

Built-in utility types express routine changes to existing types without redefining their structure by hand. For example, Pick<T, K> selects named properties from a type, while Partial<T> makes its properties optional.

type User = {
  id: string;
  name: string;
  email: string;
};

type UserLabel = Pick<User, "id" | "name">;
type UserUpdate = Partial<Pick<User, "name" | "email">>;

UserLabel contains only the identifier and name. UserUpdate allows either selected update field to be omitted. This can make the intended transformation clearer than duplicating a type, but it is worth checking that the resulting optionality matches the API or operation you are modeling. The official Utility Types reference documents the built-in options.

7. Keep type assertions rare and deliberate

An assertion tells the compiler to treat a value as a specified type; it does not check or reshape the value at runtime. As the TypeScript Handbook puts it, “A type assertion is like a type cast in other languages, but it performs no special checking or restructuring of data.”

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This assertion may silence an error, but it does not make the value a valid user:

type User = { id: string; name: string };

const user = JSON.parse(responseText) as User;

If the parsed data is missing name or has an incompatible id, the assertion does not detect the problem. At an untrusted boundary, validate the value at runtime before treating it as a User. Reserve assertions for cases where the value’s shape is already established by some other reliable mechanism but the compiler cannot infer it.

The Handbook’s basic types material explains the limits of assertions.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.