TypeScript Development

Home Technologies TypeScript Development

Overview

TypeScript is JavaScript with a type system — the same language that runs in browsers and Node.js, extended with static type annotations that the TypeScript compiler checks at build time. The type system catches a significant class of bugs before the code runs: calling a function with the wrong argument type, accessing a property that does not exist on an object, passing null where a non-null value is expected, mismatching the shape of an API response with the code that processes it. These are the errors that surface as runtime exceptions in JavaScript but as build failures in TypeScript — caught during development rather than in production.

TypeScript is our default for all JavaScript-based development. Every React frontend, every Next.js application, every Node.js backend service we build is TypeScript. The investment in type annotations pays its cost in every subsequent development session — in the autocomplete that knows what properties an object has, in the refactoring that the compiler validates rather than the developer needing to test manually, in the API boundary between frontend and backend that is enforced by shared types rather than assumed to be correct.

The TypeScript compiler is not just a type checker — it is a documentation system, a refactoring tool, and an IDE integration layer. When the types are correct, the IDE knows exactly what a function returns, what properties an object has, what arguments a method accepts. This context makes development faster and more confident. When the types are wrong or missing, the IDE provides guesses rather than facts, and the developer is back to the same uncertainty that untyped JavaScript provides.


What TypeScript Development Covers

Type system fundamentals. The TypeScript type system applied correctly to the data structures and function signatures that production code uses.

Primitive and object types: string, number, boolean, null, undefined, and the object types that describe the shape of structured data. The interface and type alias that define named shapes. The difference between interface (extensible, can be used for class contracts) and type (more powerful for union and intersection types, cannot be reopened). Readonly properties and arrays that prevent accidental mutation. Optional properties with ? that distinguish between "this property may be absent" and "this property is present but may be null".

Union and intersection types: the union A | B that accepts either type A or type B, and the intersection A & B that requires both. Discriminated unions — the pattern where a common literal property distinguishes between variants — that TypeScript's narrowing understands, allowing code within each branch to safely access the properties specific to that variant. The discriminated union that models API response variants, state machine states, and domain events.

Generic types: the parameterised types that work across multiple concrete types — the Array<T> that works for arrays of any element type, the Promise<T> that works for async operations returning any value type, the custom Result<T, E> that models success and failure outcomes with typed payloads. Generic functions and classes that capture the relationship between input and output types, allowing the type checker to verify that a function used to transform data is applied correctly at every call site.

Type narrowing: TypeScript's flow analysis that refines types within conditional branches — the typeof check that narrows string | number to string, the instanceof check that narrows Shape to Circle, the discriminant check that narrows a union to a specific variant, the in check that confirms a property exists on an object. The narrowing patterns that allow null checks, type guards, and assertion functions to teach TypeScript what a value's type is in each code path.

Utility types: the built-in type transformations that reduce boilerplate — Partial<T> for making all properties optional, Required<T> for making all properties required, Pick<T, K> for selecting a subset of properties, Omit<T, K> for excluding specific properties, Readonly<T> for making properties immutable, Record<K, V> for typed dictionaries. The utility types that compose into the complex type relationships that real APIs and domain models require.

Strict mode. The TypeScript compiler's strictest mode — enabled with "strict": true in tsconfig.json — that activates the full set of type checks that make TypeScript most effective.

strictNullChecks: the check that requires explicit handling of null and undefined rather than allowing them to propagate silently through the type system. With strictNullChecks enabled, every function that might return null must declare it in its return type, and every caller must handle the null case. The strict null checking that eliminates an entire class of null reference errors from TypeScript code.

noImplicitAny: the check that prevents TypeScript from silently inferring any for untyped code — requiring every value to have an explicit type rather than falling back to the untyped behaviour of JavaScript. The check that ensures TypeScript's type safety covers the entire codebase rather than having any-typed holes where the type system provides no safety.

strictFunctionTypes: the check that enforces correct variance for function type assignments — preventing the function type mismatches that allow type-unsafe callbacks to be passed where the type system should prevent them.

API type sharing between frontend and backend. The TypeScript capability that eliminates the class of bugs where the frontend code's understanding of an API response shape diverges from the actual response shape.

Shared type packages: the TypeScript types for API request and response shapes defined once in a shared package and imported by both the backend (to type the response construction) and the frontend (to type the response consumption). The schema changes that propagate as type errors to all consumers when the shared types are updated, making breaking API changes visible at compile time rather than at runtime.

Zod schemas with TypeScript inference: the runtime validation schema that both validates request data at runtime and infers TypeScript types for the validated data. z.infer<typeof schema> produces the TypeScript type that matches the Zod schema, keeping the runtime validation and the static type aligned automatically. The Zod schema that serves as the single source of truth for both runtime safety and compile-time safety.

tRPC for end-to-end type safety: the framework that infers the TypeScript types of API procedures on the client from the TypeScript types of the server implementation — eliminating the API client code entirely for internal APIs where the client is a TypeScript application. The tRPC router that provides full TypeScript completion and type checking for API calls without code generation.

Module organisation and project structure. TypeScript project organisation that scales with application complexity.

Barrel exports: the index.ts re-export files that expose a module's public API through a single import path, hiding internal implementation details and providing a stable public surface. The barrel pattern applied judiciously — useful for packages and well-defined modules, counterproductive when applied mechanically to every directory.

Path aliases: the tsconfig.json path configuration that allows imports like @/components/Button rather than ../../components/Button — shorter, more readable import paths that do not break when files are moved. The path alias configuration that is consistent between the TypeScript compiler and the build tool (Vite, webpack, Next.js).

Declaration files: the .d.ts type declaration files for JavaScript libraries that do not include TypeScript types — either from @types/ packages maintained by the DefinitelyTyped community or written locally for libraries without published types. The declaration file that tells TypeScript what a JavaScript module exports so that TypeScript code can use it with type safety.

TypeScript in the build pipeline. The compiler and tooling configuration that produces correct, optimised output.

tsconfig.json configuration: the target ECMAScript version that determines what JavaScript syntax the TypeScript compiler produces, the module system (CommonJS for Node.js, ESM for modern bundlers), the strict mode settings, the path aliases, and the include and exclude patterns that tell the compiler which files to compile. Separate tsconfig.json files for different contexts — the base config, the app config that extends it, the test config with test-specific settings.

tsc for type checking: running the TypeScript compiler in type-check-only mode (--noEmit) as a CI step that validates type correctness across the entire codebase without producing output files. The CI check that catches type errors in code that the test suite does not exercise.

esbuild and swc for transpilation: the fast JavaScript transpilers that handle TypeScript syntax stripping without performing type checking — used in development and production builds where build speed matters and where type checking runs separately. The separation between type checking (slow, comprehensive) and transpilation (fast, syntax-only) that keeps development iteration fast while maintaining type safety.

Testing with TypeScript. The test infrastructure that maintains type safety in test code.

Vitest and Jest with TypeScript: test frameworks configured to run TypeScript test files — the ts-jest transform for Jest or Vitest's native TypeScript support. The typed test utilities that provide correct types for mock functions, test doubles, and assertion results.

Type-safe mocks: the typed mock factory that produces mock objects satisfying a TypeScript interface, with the mock's method signatures checked by TypeScript to match the interface they are replacing. The mock that cannot accidentally call a method with wrong argument types because TypeScript checks the mock's type.

Typed test data builders: the factory functions that produce typed test data with sensible defaults and the ability to override specific properties for each test. The builder pattern that produces correctly typed test fixtures without the boilerplate of constructing complete objects in every test.


TypeScript Across Our Stack

TypeScript is the language for every JavaScript context in our projects — not just the React frontend, but also the Node.js backend services, the Next.js API routes, the database query builders, the CLI tooling, and any JavaScript-executed configuration.

React and Next.js. The React component type system — typed component props with React.FC<Props> or function component signatures, typed event handlers that know the event target's shape, typed refs and context. The Next.js page types — GetServerSideProps, GetStaticProps, GetStaticPaths for pages router pages, the typed server component props and generateMetadata for the App Router. The typed hooks — useState<Type> for typed state, useReducer with typed action and state, custom hooks with typed return values.

Node.js services. TypeScript for Node.js HTTP services — the typed request and response objects in Express and Fastify handlers, the typed middleware that augments the request object with authenticated user data, the typed service layer with interfaces for dependencies. The @types/node types for Node.js built-in modules.

Database query builders. Prisma and Drizzle with TypeScript — the ORM and query builder that generate TypeScript types from the database schema, making database queries type-safe. The Prisma client that knows the shape of every model and relation, and the Drizzle query builder that types SQL query results. The schema.ts or prisma/schema.prisma that is the single source of truth for both the database schema and the TypeScript types derived from it.


Technologies Used

  • TypeScript 5.x — current language version with the latest type system features
  • tsconfig.json — compiler configuration with strict mode
  • Zod — runtime validation with TypeScript type inference
  • tRPC — end-to-end type-safe API for TypeScript fullstack applications
  • React 18/19 — typed component development
  • Next.js 14/15 — typed full-stack framework
  • Express / Fastify — typed Node.js HTTP frameworks
  • Prisma / Drizzle — typed database access
  • Vitest / Jest — typed testing frameworks
  • esbuild / swc — fast TypeScript transpilation
  • ESLint with typescript-eslint — TypeScript-aware linting
  • Prettier — code formatting
  • ts-node / tsx — TypeScript execution for scripts and development

TypeScript as the Default, Not the Exception

The question of whether to use TypeScript in a JavaScript project is settled for us — the answer is always yes, unless there is a specific constraint that prevents it. The overhead of TypeScript in a well-configured project is minimal: the type annotations that document what the code does, the tsconfig that enables strict checks, the build step that was already there. The benefits — the errors caught at build time rather than runtime, the IDE support that makes navigation and refactoring reliable, the shared types that keep frontend and backend aligned — compound over the project's lifetime.

TypeScript does not make it impossible to write incorrect code. A type system cannot verify business logic. But it eliminates an important category of structural errors — the wrong type, the missing property, the null that was not expected — that are disproportionately expensive to debug because they surface at runtime rather than at the moment the mistake is made.


Type Safety Across the Full JavaScript Stack

TypeScript development from schema definition through backend services, API boundaries, and frontend components — with strict mode enabled, shared types across the stack, and the tooling configuration that makes type checking a reliable part of the development and CI workflow rather than an optional afterthought.