Type System Architecture
Sightline uses a comprehensive, nested type system designed to capture the full complexity of incident 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
Incident (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 are organized by domain:
src/core/types/
├── index.ts # Re-exports all types
├── incident.ts # Core Incident interface
├── temporal.ts # Time-related types
├── locations.ts # Location and geography
├── classification.ts # Hynek, Vallee, etc.
├── witnesses.ts # Witness information
├── evidence.ts # Sensor and physical evidence
├── objects.ts # Object characteristics
├── movement.ts # Movement patterns
├── investigation.ts # Investigation status
├── response.ts # Response and impact
├── sources.ts # Source documentation
├── weather.ts # Environmental conditions
├── relations.ts # Related incidents
├── aviation.ts # Aviation-specific fields
├── filters.ts # Filter state types
├── config.ts # Configuration types
├── provider.ts # Data provider interfaces
├── renderer.ts # Renderer prop interfaces
└── helpers.ts # Type utilitiesCore Incident Type
// src/core/types/incident.ts
export interface Incident {
// Identity (required)
id: string;
status: PublicationStatus;
createdAt: string;
updatedAt: string;
// Required sections
temporal: TemporalData;
location: LocationData;
// Optional summary
summary?: string;
// Optional detailed sections
classification?: ClassificationData;
witnesses?: WitnessData;
sensorEvidence?: SensorEvidenceData;
objectCharacteristics?: ObjectCharacteristics;
movement?: MovementData;
investigation?: InvestigationData;
responseImpact?: ResponseImpactData;
sourceData?: SourceData;
environment?: EnvironmentalConditions;
media?: MediaAttachment[];
relations?: RelationalData;
}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 incident data:
// src/core/types/helpers.ts
// 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 incident types for specific use cases.
Display Types
For UI rendering, we extend the base type with computed properties:
// Incident with computed display properties
interface IncidentDisplay extends Incident {
// 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 incident structure:
interface IncidentFilters {
// Date filtering
dateRange?: { start: string; end: string };
// Location filters (arrays for multi-select)
countries?: string[];
siteTypes?: string[];
locationSensitivities?: LocationSensitivity[];
// Classification filters
incidentTypes?: 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 IncidentFilters {
// ...
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 incident: Incident = {
id: "inc-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(incident);
// Deserializes with type safety
const parsed: Incident = JSON.parse(jsonString);Validation
Runtime validation using Zod schemas:
import { validateIncident, validateIncidentArray } from '@/core/schemas';
// Validate a single incident (throws on invalid)
const incident = validateIncident(unknownData);
// Validate an array (filters out invalid, returns stats)
function processData(data: unknown[]): Incident[] {
const { valid, invalidCount, errors } = validateIncidentArray(data);
if (invalidCount > 0) {
console.warn(`Filtered ${invalidCount} invalid incidents:`, errors);
}
return valid;
}Best Practices
- Always use types - Never use
anyfor incident 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 { Incident, LocationData } from '@disclosureos/sightline-core';
// Avoid
import * as Types from '@disclosureos/sightline-core';