rule-frontend-types
MANDATORY when editing files matching ["frontend/src/**/*.ts", "frontend/src/**/*.tsx", "frontend/src/types/**/*"]. TypeScript type organization for the Simpler Grants frontend — when to share vs inli
SKILL.md.claude/skills/rule-frontend-types/SKILL.md
GitHubFrontend Types Rules
Where Types Live
- Shared types →
frontend/src/types/. Use a domain subfolder (application/,opportunity/,search/,attachment/,grantor/) when the slice has multiple related files; otherwise a flat<feature>Types.tsfile at the root oftypes/. - Component-local types (props, internal state used by exactly one component) → inline in the component file or a sibling
Component.types.ts. NEVER pollutesrc/types/with single-use prop types. - Ambient/global declarations →
*.d.ts(e.g.,i18n.d.ts).
Decision rule: if a type is imported by 2+ files, or crosses a feature boundary, promote it to
src/types/.
File Naming
camelCaseending inTypes.tsfor shared modules:opportunityTypes.ts,apiResponseTypes.ts.*ResponseTypes.tsspecifically for API response shapes:applicationResponseTypes.ts,competitionsResponseTypes.ts,formResponseTypes.ts.*.d.tsfor ambient declarations only.- AVOID
index.tsbarrels unless a domain folder genuinely needs one.
Type Identifier Naming
ALWAYS use PascalCase for type and interface (QueryParamData, PaginationInfo, APIResponse).
Suffix conventions:
*Response— raw API response envelopes (OpportunityResponse)*Request/*Params— outbound request payloads or query params (QueryParamData)*Props— React component props (ErrorProps)*State— reducer/store/local state shapes*Dtois not used in this codebase — do NOT introduce it.
NEVER prefix interfaces with I. NEVER prefix type aliases with T. Booleans use is*, has*, should*.
Property Naming — Wire Format vs Internal
- API response/request types mirror the backend exactly using
snake_case(page_offset,pagination_info,status_code). Do NOT camelCase API contract types — it breaks the wire mapping. - All other types use
camelCaseproperties. - When mapping API data into UI state, do the snake→camel conversion at the boundary (fetcher/service layer) and define a separate UI type — NEVER reuse the response type as the UI type.
// Wire (snake_case) — mirrors backend
export interface PaginationInfo {
page_offset: number;
page_size: number;
total_pages: number;
sort_direction: "asc" | "desc";
}
// UI (camelCase) — converted at the service boundary
export interface Pagination {
pageOffset: number;
pageSize: number;
totalPages: number;
sortDirection: SortDirection;
}
type vs interface
- DEFAULT to
interfacefor object shapes that may be extended (props, response envelopes — matches existing usage likeinterface APIResponse,interface ErrorProps). - USE
typefor unions, intersections, mapped/conditional types, tuples, and function signatures. - NEVER mix both for the same concept.
Generics
PREFER a properly generic API envelope for new code:
export interface APIResponse<TData = unknown> {
data: TData;
pagination_info?: PaginationInfo;
warnings?: ApiMessage[];
errors?: ApiMessage[];
status_code: number;
}
Today's
APIResponseusesdata: unknown[] | object. New code should pass an explicitTData, and existing call sites should be tightened opportunistically.
- Single-letter generics only when meaning is obvious (
T,K,V). Otherwise use descriptive names:TData,TParams,TError. - CONSTRAIN generics (
<T extends object>) rather than leaving them open when shape matters. - DO NOT reach for generics when a plain type works — avoid generic gymnastics.
Discriminated Unions
PREFER explicit discriminants over optional-field flag soup:
type FetchResult<T> =
| { status: "success"; data: T }
| { status: "error"; error: FrontendErrorDetails }
| { status: "loading" };
function render<T>(result: FetchResult<T>) {
switch (result.status) {
case "success": return renderData(result.data);
case "error": return renderError(result.error);
case "loading": return renderSpinner();
default: {
const _exhaustive: never = result;
return _exhaustive;
}
}
}
- Use a
kindorstatusliteral string as the discriminant. - Exhaustively
switchwith aneverdefault for safety. - NEVER overload one interface with mutually exclusive optional fields when a union expresses intent more clearly.
Enums vs Union Literals
- Existing code uses
enum Breakpointsfor fixed design-token sets — fine when values are referenced by name across many files. - For most other finite sets (status, sort direction), PREFER string literal unions:
type SortDirection = "asc" | "desc". They tree-shake and serialize naturally. - NEVER introduce numeric enums.
Imports & Re-exports
- Import shared types via the
src/types/...path (match the alias configured intsconfig.json— verify before writing examples). - USE
import type { … }for type-only imports to keep runtime bundles clean. - AVOID deep barrel files; import directly from the specific module.
import type { APIResponse, PaginationInfo } from "src/types/apiResponseTypes";
import type { ErrorProps } from "src/types/uiTypes";
Ambient & Third-Party Augmentation
Module augmentation (e.g., extending next-intl, NewRelic, next/server) belongs in a *.d.ts at src/types/ — see intl.ts, newRelic.ts, i18n.d.ts for the established pattern. ALWAYS include a top-of-file comment explaining what is being augmented and why.
Anti-Patterns
- NO
any— useunknownand narrow. - NO
ascasts to bypass type errors; fix the type or use a type guard. - NO duplicating an API response type as a UI type — define both and map at the boundary.
- NO single-use prop types in
src/types/. - NO
I-prefixed interfaces, noT-prefixed type aliases. - NO camelCasing API contract fields.
- NO numeric enums.
- NO giant union of optional fields where a discriminated union belongs.
Related Rules
frontend-services.mdc— service-layer boundary where snake→camel mapping happensfrontend-components.mdc— where*Propstypes are declared and consumedapi-routes.mdc— backend wire format that response types must mirror
Specialist Validation
Simple changes (add a field, new union literal): No specialist needed.
Moderate changes (new shared type module, new response envelope): Invoke codebase-conventions-reviewer.
Complex changes (new generic API envelope, refactoring discriminated unions, ambient module augmentation): Invoke in parallel:
kieran-typescript-reviewer— TypeScript-specific quality reviewarchitecture-strategist— validate type boundaries and module placement