Your Sunday, upgraded. Free live AI class · 5 PM ISTReserve your seat
PROGRAMMING LANGUAGES

TypeScript in a Real Project: Put Types at Every Boundary

TypeScript pays off when types describe the data that actually flows through your application. This guide walks through a course-enrolment feature and shows where to define types, where to validate and how to keep the two in step.

On this page

Why boundaries matter more than annotations

Adding type annotations to every variable does not make a project safe. TypeScript already infers most local types. The places that need deliberate attention are boundaries, where data enters or leaves a part of the system: HTTP requests and responses, database rows, environment variables, browser storage and messages between services. If the types at those points are accurate, inference carries correctness through the rest of the code.

The running example is an enrolment feature: a learner submits a form, an API route validates it, a service writes to the database and the response updates the page.

Define domain types once

Start with types that describe the domain in your own words, independent of any framework. Keep them in one module so the API, services and UI share them.

export type CourseId = string & { readonly brand: "CourseId" };
export type Enrolment = {
  courseId: CourseId;
  learnerEmail: string;
  plan: "self-paced" | "live";
  enrolledAt: Date;
};

The branded CourseId prevents a plain string, such as a learner ID, from being passed where a course ID is expected. Use branding sparingly, for identifiers that are easy to confuse.

Validate where data enters

The request body arrives as unknown data. Parse it with a schema that checks every field and returns either a typed value or a list of errors. Schema libraries let you derive the TypeScript type from the schema, so the validation and the type cannot drift apart.

Return a 400 response with field-level messages when validation fails. Inside the service, accept only the validated type; the service should never see unchecked input.

  • Request bodies and query parameters: validate in the route handler.
  • Environment variables: validate once at startup and export a typed config.
  • Third-party API responses: validate before mapping into domain types.
  • Database rows: map from the driver's row type into domain types in one repository module.

Model states with unions

The enrolment form can be idle, submitting, successful or failed. Represent that with a discriminated union rather than several optional fields, so impossible combinations such as 'submitting and failed' cannot be represented. A switch on the status field, with an exhaustiveness check, ensures the UI handles every state.

type SubmitState =
  | { status: "idle" }
  | { status: "submitting" }
  | { status: "success"; enrolment: Enrolment }
  | { status: "error"; message: string };

Keep the compiler strict

Enable strict mode in tsconfig.json, and consider noUncheckedIndexedAccess, which makes array and record lookups return possibly undefined values. These settings surface real bugs, particularly around missing data. Treat new any types and as assertions as review topics: each one is a place where the compiler has stopped checking.

When an external library has weak types, wrap it in a small module with precise types at its edge, instead of letting loose types spread through the codebase.

Test the boundaries

Unit-test the validators with valid input, each missing field and wrong types. Test the repository mapping with a row that contains NULL values. Type checking catches mismatches between modules; runtime tests catch mismatches between your types and the real world, which the compiler cannot see.

Checklist for a typed feature

  • Domain types live in one shared module
  • Every external input is validated before use
  • Types are derived from schemas where possible
  • UI and workflow states are discriminated unions
  • Strict compiler options are enabled
  • any and as assertions are rare, reviewed and justified

Share or cite this guide

Innovate with Ajay Editorial (2026-10-07). TypeScript in a Real Project: Put Types at Every Boundary. https://innovatewithajay.com/articles/typescript-types-at-every-boundary

Editorial approach, sources and corrections

YOUR NEXT CHAPTER

You have the curiosity.
Let’s turn it into capability.

Start with a practical workshop or bring a specific question to Ajay.

Chat with Ajay