Type System Architecture
Atlas uses a comprehensive, nested type system designed to capture the full complexity of event data while remaining flexible for different use cases.
Design Principles
- Nested over flat - Related data is grouped into logical sections
- Required minimum, optional maximum - Core fields required, everything else optional
- Typed enumerations - String literals for consistency and autocomplete
- Extensible - Easy to add new fields without breaking changes
Type Hierarchy
MapEvent (root)
├── id, status, timestamps (required)
├── temporal (required)
│ ├── date, dateCertainty (required)
│ └── time, duration, timezone... (optional)
├── location (required)
│ ├── name, country, lat, lng (required)
│ └── altitude, airspace... (optional)
└── [optional sections]
├── classification
├── witnesses
├── sensorEvidence
├── objectCharacteristics
├── movement
├── investigation
├── responseImpact
├── sourceData
├── environment
├── media
└── relationsFile Organization
Types come from the shared @disclosureos/mapping package (and related packages like @disclosureos/evidence, @disclosureos/core). Runtime validation lives in Atlas:
apps/atlas/src/core/
├── schemas/ # Zod validation (strict + lenient input with coercion)
│ ├── event.ts # MapEventSchema, MapEventInputSchema, alias maps
│ └── index.ts # Schema and alias exports
├── config.ts
├── data-context.tsx
└── ...Import types from the mapping package and validation from @/core/schemas:
import type { MapEvent, LocationData, MapEventType } from '@disclosureos/mapping';
import { validateMapEventArray, MapEventInputSchema } from '@/core/schemas';Core MapEvent Type
The root type is defined in @disclosureos/mapping:
// @disclosureos/mapping
export interface MapEvent {
id: string;
status: PublicationStatus;
createdAt: string;
updatedAt: string;
temporal: TemporalData;
location: LocationData;
summary?: string;
classification?: ClassificationData;
witnesses?: WitnessData;
sensorEvidence?: SensorEvidenceData;
objectCharacteristics?: ObjectCharacteristics;
movement?: MovementData;
investigation?: InvestigationData;
responseImpact?: ResponseImpactData;
sourceData?: SourceData;
environment?: EnvironmentalConditions;
media?: MediaAttachment[];
relations?: RelationalData;
// ... other optional sections
}Enumeration Strategy
We use string literal unions for type safety:
// Instead of:
enum LocationSensitivity {
Critical = 'critical',
High = 'high',
// ...
}
// We use:
type LocationSensitivity = 'critical' | 'high' | 'moderate' | 'standard';Benefits:
- Better tree-shaking
- No runtime enum object
- Direct JSON compatibility
- Easy to extend
Nested Section Pattern
Each section follows a consistent pattern:
// 1. Define the data interface
interface WitnessData {
witnessCount: number;
witnessCategories: WitnessCategory[];
credibilityFactors?: CredibilityFactor[];
// ...
}
// 2. Define related enumerations
type WitnessCategory =
| 'civilian'
| 'military'
| 'pilot_commercial'
// ...
// 3. Export all from section file
export type { WitnessData, WitnessCategory, CredibilityFactor };Utility Types
The helpers.ts file provides generic utility types for working with event data:
// Type utilities (conceptual; implement in your app or mapping package)
// Make specific fields required
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Make specific fields optional
type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Deep partial - makes all nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Make all fields non-nullable
type NonNullableFields<T> = {
[P in keyof T]: NonNullable<T[P]>;
};These utility types are useful for creating variations of event types for specific use cases.
Display Types
For UI rendering, we extend the base type with computed properties:
// MapEvent with computed display properties
interface MapEventDisplay extends MapEvent {
// Coordinates for map rendering [longitude, latitude]
coordinates: [number, number];
// Sensitivity display properties
sensitivityColor?: string;
sensitivityLabel?: string;
// Formatted date string
formattedDate: string;
// Age calculation for sorting/filtering
ageInDays: number;
}Filter Types
Filters mirror the event structure:
interface MapEventFilters {
// Date filtering
dateRange?: { start: string; end: string };
// Location filters (arrays for multi-select)
countries?: string[];
siteTypes?: string[];
locationSensitivities?: LocationSensitivity[];
// Classification filters
eventTypes?: string[];
hynekClassifications?: string[];
valleeClassifications?: string[];
// Evidence filters
detectionMethods?: string[];
evidenceTypes?: string[];
// Quick filters (booleans)
hasRadarConfirmation?: boolean;
hasMultipleWitnesses?: boolean;
hasMilitaryWitness?: boolean;
hasPhysicalEvidence?: boolean;
hasOfficialInvestigation?: boolean;
hasVideoEvidence?: boolean;
}Extending Types
To add new fields:
// 1. Add to the appropriate section type
interface WitnessData {
witnessCount: number;
witnessCategories: WitnessCategory[];
// NEW: Add new field
anonymousWitnesses?: number;
}
// 2. Add enumeration values if needed
type WitnessCategory =
| 'civilian'
| 'military'
| 'journalist' // NEW
// ...
// 3. Update filters if filterable
interface MapEventFilters {
// ...
witnessCategories?: WitnessCategory[]; // NEW
}
// 4. Update display formatting if needed
function formatWitnesses(data: WitnessData): string {
// Include new field in display
}JSON Compatibility
All types are designed for direct JSON serialization:
// Valid JSON that matches our types
const mapEvent: MapEvent = {
id: "evt-001",
status: "published",
createdAt: "2024-01-15T00:00:00Z",
updatedAt: "2024-01-15T00:00:00Z",
temporal: {
date: "2024-01-15",
dateCertainty: "exact"
},
location: {
name: "Phoenix, AZ",
country: "United States",
latitude: 33.4484,
longitude: -112.074,
siteType: "urban",
locationSensitivity: "standard"
}
};
// Serializes cleanly
JSON.stringify(mapEvent);
// Deserializes with type safety
const parsed: MapEvent = JSON.parse(jsonString);Validation
Runtime validation using Zod schemas in @/core/schemas (see event.ts).
Two-tier schema architecture
Atlas uses two schema tiers so adapters get consistent behavior:
- Strict schemas (
MapEventSchema,LocationDataSchema, etc.) — Validate canonical enum values only. Use these when you need to reject any non-canonical input (e.g. internal checks, type exports). - Lenient input schemas (
MapEventInputSchema,LocationDataInputSchema, etc.) — Wrap enum fields withz.preprocess()and alias maps so common shorthands (e.g."radar","photo") are coerced to canonical values before validation. Used at the adapter boundary.
All adapters (static, Supabase, API) call validateMapEventArray(), which parses with MapEventInputSchema. Coercion therefore happens in one place and applies to every data source. To validate without coercion, use validateMapEventArrayStrict().
import { validateMapEvent, validateMapEventArray, validateMapEventArrayStrict } from '@/core/schemas';
// Validate a single event (throws on invalid)
const mapEvent = validateMapEvent(unknownData);
// Validate an array with alias coercion (recommended at adapter boundary)
function processData(data: unknown[]): MapEvent[] {
const { valid, invalidCount, errors } = validateMapEventArray(data);
if (invalidCount > 0) console.warn(`Filtered ${invalidCount} invalid events:`, errors);
return valid;
}
// Strict validation — no coercion; rejects non-canonical enum values
const { valid } = validateMapEventArrayStrict(data);Best Practices
- Always use types - Never use
anyfor event data - Check optional fields - Use optional chaining (
?.) - Narrow types - Use type guards before accessing nested data
- Prefer strict imports - Import specific types, not
*
// Good
import type { MapEvent, LocationData } from '@disclosureos/mapping';
// Avoid
import * as Types from '@disclosureos/mapping';