Repository navigation
Types
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.
What you'll learn:
- Declare shared types in the global
types/directory - Understand how
RowIdhandles SQLite's bigint behavior - Use
declare globalto extend Express request types
StartER uses TypeScript to guarantee a strict, shared contract between your database, API, and components.
The src/types/index.d.ts ambient file re-exports resource types directly from their Single Source of Truth Zod schema files (*Schemas.ts). This makes entity types accessible globally across both backend and frontend without explicit imports or zod dependencies in ambient declarations.
// src/types/index.d.ts
type Item = import("../express/modules/item/itemSchemas").Item;
type User = import("../express/modules/user/userSchemas").User;In src/express/modules/item/itemSchemas.ts, the type is inferred directly from the master Zod schema:
export const ItemSchema = z.object({
id: z.number(),
title: z.string().max(255),
user_id: z.number(),
});
export type Item = z.infer<typeof ItemSchema>;Types defined in src/types/index.d.ts are automatically available throughout the application.
In React components, these types are used directly. For example, in src/react/components/item/ItemList.tsx:
const items = use(getOrFetch<Item[]>("/api/items"));Here, Item[] indicates that the items variable contains an array of objects conforming to the Item type.
Tip
The simple signature getOrFetch<Item[]>("/api/items") is shown here for type inference illustration. For paginated resources, see Pagination for the options-based getOrFetch signature.
Similarly, in Express modules, repositories import master entity schemas to parse database rows safely. For example, in src/express/modules/item/itemRepository.ts:
import { ItemSchema } from "./itemSchemas";
// ...
findAll(limit: number, offset: number): Item[] {
// ...
return rows.map((row) => ItemSchema.parse(row));
}The same Item type is shared across Express and React. Parsing SQL rows through ItemSchema.parse(row) guarantees runtime type safety.
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 existsThus, 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.
-
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. -
One declaration = one responsibility: each module must extend
Requestonly for its needs. If a module extendsRequestwith properties it doesn't own, types become unpredictable for other modules. -
Name properties consistently (like
req.item,req.user...): naming conventions make code easier to read and reduce typos (reliable autocomplete). -
Avoid name collisions between modules: two modules using the same property name on
Requestwould produce a silent type conflict. -
Document why a property is added: the
declare globalblock is invisible in the file that consumes it, so a comment explaining the need helps the next contributor (often yourself). -
Type-check component form schemas with ambient types: annotate component Zod schemas with
z.ZodType<Pick<Entity, "field">>to ensure frontend form schemas catch entity property changes at compile-time without importing backend runtime code.
AI co-creation
Getting started
Explanations
How-To Guides
Reference
Digging deeper