Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
TypeScript Type System
This document explains the core TypeScript patterns, utility types, and interfaces used across the GrantMaster codebase.🏗️ Type Hierarchy Visual
🛠️ Core Utility Types
We use a set of global utility types to ensure consistency across services.1. TenantScoped
Every document belonging to a tenant should inherit from this.
2. WithId<T>
Adds an id field to a type (useful for Firestore docs where the ID is the document name).
3. AsyncResult<T>
Standardized response for async operations.
📂 Type Directory Structure
src/schemas/: Canonical persisted entity schemas and inferred types.src/features/{feature}/contracts.ts: Feature-owned domain contracts.src/shared/*/*.contracts.tsandsrc/core/*.contracts.ts: Cross-feature and infrastructure contracts.src/core/firestoreCollections.ts: Mapping of collection IDs to their TypeScript interfaces.packages/shared/: Common utility types (Address,Attachment,BankAccount, etc.) shared with Cloud Functions.
🔄 Type Conversion & Mapping
We follow a strict “DTO to Entity” pattern in the service layer:- Incoming (API/Scraper): Raw JSON or third-party types.
- Storage (Firestore): Flat, indexed documents.
- Domain (Service): Rich objects with derived fields (e.g.,
matchScore). - Presentation (UI): Truncated or formatted strings for display.
🔄 DTO-to-Entity Flow (Full Example)
The type system enforces a four-layer boundary. Here is a concrete walk-through using theExpense domain:
Layer responsibilities
Incoming (form / API): Raw string inputs validated by Zod schemas insrc/schemas/. Zod coerces strings to numbers/dates and strips unknown fields before any service call.
Storage (Firestore): Flat documents using Firestore Timestamp types. Stored in canonical snake_case in Firestore, mapped to camelCase TypeScript types by firestoreCollections.ts.
Domain (service output): WithId<T> types with derived fields added by service methods (e.g., matchScore on GrantOpportunity, burnRate on BudgetForecast). Timestamps converted to Date.
Presentation (UI): Display-only types in feature components/ — formatted strings, badge labels, relative times. Never passed back to services.
Discriminated Unions
Discriminated unions are used wherever an entity can be in fundamentally different shapes based on atype or status discriminant.
Example: SystemEvent<T>
Example: ServiceResult<T>
All service methods return ServiceResult<T> — a discriminated union of success and failure:
Example: NotificationPayload
Naming Conventions
Files
| Pattern | Convention | Example |
|---|---|---|
| Domain types | camelCase.ts | grants.ts, hr.ts |
| Zod schemas | camelCase.schema.ts | projects.schema.ts, grants.schema.ts |
| Barrel exports | index.ts | Avoid root barrels; export from the owning module |
| Ambient declarations | *.d.ts | global.d.ts, firebase-functions.d.ts |
Types and Interfaces
| Pattern | Convention | Example |
|---|---|---|
| Domain entity | PascalCase | Project, ActiveGrant, JournalEntry |
| Enum | PascalCase | ProjectStatus, UserRole |
| Union string literal type | PascalCase | PipelineStage, BudgetCategory |
| Zod schema variable | PascalCase + Schema | ProjectSchema, ExpenseSchema |
| Inferred Zod type | z.infer<typeof XSchema> | exported from the owning schema or feature contract |
| DTO (input to a service) | Create{Entity}DTO / Update{Entity}DTO | CreateExpenseDTO, UpdateProjectDTO |
| Display/view model | {Entity}Row / {Entity}Card | ExpenseRow, GrantCard |
Firestore ↔ TypeScript naming
Firestore collection names use pluralsnake_case; TypeScript types use singular PascalCase. The mapping is declared in src/core/firestoreCollections.ts:
Rule: If the collection name differs from what you’d expect from the entity name, the mapping must be documented both here and in the domain model naming gotchas table.
Schema Layer (src/schemas/)
Zod schemas in src/schemas/ are the canonical validation layer. Persisted entity types should be derived from schemas using z.infer<>, while non-persisted domain contracts should live beside the owning feature or shared/core module.
Schema validation in services
🛡️ Enforcing Type Safety
- No
any: The use ofanyis strictly prohibited. Useunknownor a generic if the type is truly dynamic. - Discriminated Unions: Used for all multi-shape types (events, service results, notification payloads).
- Strict Null Checks: All optional fields must be explicitly marked with
?and handled in the UI. - Zod first: All external input (forms, API responses, Firestore reads via converters) passes through Zod before touching domain types.
WithId<T>discipline: Raw Firestore documents are typed asT; after reading and attaching the document ID, they becomeWithId<T>. Never mutate the source document to addid.