Skip to content
rocambille edited this page Jun 4, 2026 · 15 revisions

Summary: Typing consistency is essential to ensure code robustness and reliability. StartER relies on TypeScript to ensure this consistency end-to-end: from client (React) to server (Express). This page introduces two fundamental typing techniques used in the framework.

The role of TypeScript

StartER uses TypeScript to guarantee a strict, shared contract between your database, API, and components.

Implementation in StartER

Shared types: src/types/index.d.ts

The src/types/index.d.ts file is the central point for defining types shared between all parts of the application. It provides a set of types that are accessible globally without the need for explicit imports.

type Item = {
  id: number;
  title: string;
  user_id: number;
};

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

Types defined here are automatically available throughout the code, without import.

In React components, these types are used directly. For example, in src/react/components/item/ItemList.tsx:

const items = use(cache<Item[]>("/api/items"));

Here, Item[] indicates that the items variable contains an array of objects conforming to the Item type.

Similarly, in Express modules, these types are used to ensure consistency. For example, in src/express/modules/item/itemRepository.ts:

const itemSchema: z.ZodType<Item> = z.object({
  id: z.number(),
  title: z.string(),
  user_id: z.number(),
});

// ...

findAll(limit: number, offset: number): Item[] {
  // ...

  return rows.map((row) => itemSchema.parse(row));
}

The same Item type is used in both Express and React, ensuring perfect consistency between the backend and frontend. The Zod schema bound to z.ZodType<Item> guarantees runtime type safety.

Declaration merging for Express Request interface

Interfaces in TypeScript are open. If an interface is defined with the name of an existing interface, its properties are added to the original. This capability, called declaration merging, allows properties to be added to an existing interface without redefining it entirely. StartER leverages this technique to enrich Express Request interface with module-specific data.

Every Express module that adds a property to the Request object does so via a declare global block. For example, in src/express/modules/item/itemParamConverter.ts:

declare global {
  namespace Express {
    interface Request {
      item: Item;
    }
  }
}

After this declaration, TypeScript recognizes req.item as a valid property of type Item on all Express Request objects in the application.

req.item = item; // OK: req.item exists

Thus, each module extends Request with what it needs, with a cascading effect on the rest of the project. The TypeScript IDE and compiler recognize the added properties as legitimate.

Best practices and use cases

  1. Keep common types in index.d.ts: these are the ones shared between Express and React. This avoids duplication and ensures that both frontend and backend share the same data contract.
  2. One declaration = one responsibility: each module must extend Request only for its needs. If a module extends Request with properties it doesn't own, types become unpredictable for other modules.
  3. Name properties consistently (like req.item, req.user...): naming conventions make code easier to read and reduce typos (reliable autocomplete).
  4. Avoid name collisions between modules: two modules using the same property name on Request would produce a silent type conflict.
  5. Document why a property is added: the declare global block is invisible in the file that consumes it, so a comment explaining the need helps the next contributor (often yourself).

See also

Clone this wiki locally