Replaced generic any types with proper GeoJSON type definitions (Point and Polygon) for all location-based fields in the database schema to improve type safety and developer experience.
import type { Point, Polygon } from 'geojson';The @types/geojson package provides standard TypeScript definitions for GeoJSON geometry types as defined by RFC 7946.
Before:
service_areas?: any[]; // GeoJSON polygons
primary_location?: any; // GeoJSON point
current_location?: any; // GeoJSON pointAfter:
service_areas?: Polygon[];
primary_location?: Point;
current_location?: Point;Fields Updated:
service_areas- Array of Polygon geometries defining mover's service coverageprimary_location- Point geometry for mover's business addresscurrent_location- Point geometry for real-time mover location
Before:
pickup_location: any; // GeoJSON point
dropoff_location: any; // GeoJSON point
tracking_data: Array<{
location: any; // GeoJSON point
...
}>;
last_tracked_location?: any; // GeoJSON pointAfter:
pickup_location: Point;
dropoff_location: Point;
tracking_data: Array<{
location: Point;
...
}>;
last_tracked_location?: Point;Fields Updated:
pickup_location- Point geometry for pickup addressdropoff_location- Point geometry for dropoff addresstracking_data[].location- Point geometry for each tracking updatelast_tracked_location- Point geometry for most recent location
Before:
location: any; // GeoJSON pointAfter:
location: Point;Fields Updated:
location- Point geometry for recorded mover location
TypeScript now enforces correct GeoJSON structure:
// ✅ Valid - proper Point structure
const booking: Booking = {
pickup_location: {
type: 'Point',
coordinates: [36.8219, -1.2921] // [longitude, latitude]
},
// ... other fields
};
// ❌ Invalid - TypeScript error
const booking: Booking = {
pickup_location: {
lat: -1.2921,
lng: 36.8219
}, // Error: Type doesn't match Point
// ... other fields
};Developers get autocomplete for GeoJSON properties:
type: 'Point' | 'Polygon'coordinates: [number, number] | [number, number][][]bbox?: [number, number, number, number]
All GeoJSON data follows RFC 7946 specification:
- Point:
{ type: 'Point', coordinates: [lng, lat] } - Polygon:
{ type: 'Polygon', coordinates: [[[lng, lat], ...]] }
Type-safe integration with mapping libraries:
- Mapbox GL JS
- Leaflet
- Google Maps
- PostGIS (via Supabase)
interface Point {
type: 'Point';
coordinates: [number, number]; // [longitude, latitude]
bbox?: [number, number, number, number]; // Optional bounding box
}Example:
const nairobiCBD: Point = {
type: 'Point',
coordinates: [36.8219, -1.2921] // Nairobi CBD
};interface Polygon {
type: 'Polygon';
coordinates: [number, number][][]; // Array of linear rings
bbox?: [number, number, number, number];
}Example:
const serviceArea: Polygon = {
type: 'Polygon',
coordinates: [[
[36.7, -1.3], // Southwest corner
[36.9, -1.3], // Southeast corner
[36.9, -1.1], // Northeast corner
[36.7, -1.1], // Northwest corner
[36.7, -1.3] // Close the ring
]]
};import type { Booking, Point } from '@/types/database';
const pickupLocation: Point = {
type: 'Point',
coordinates: [36.8219, -1.2921] // Nairobi CBD
};
const dropoffLocation: Point = {
type: 'Point',
coordinates: [36.7073, -1.3044] // Kibera
};
const booking: Partial<Booking> = {
pickup_address: '123 Kenyatta Avenue, Nairobi',
pickup_location: pickupLocation,
dropoff_address: '456 Kibera Drive, Nairobi',
dropoff_location: dropoffLocation,
// ... other fields
};
// Insert into database
const { data, error } = await supabase
.from('bookings')
.insert(booking);import type { Mover, Polygon } from '@/types/database';
// Define service area covering Nairobi County
const nairobiServiceArea: Polygon = {
type: 'Polygon',
coordinates: [[
[36.6, -1.4],
[37.1, -1.4],
[37.1, -1.1],
[36.6, -1.1],
[36.6, -1.4]
]]
};
const moverUpdate: Partial<Mover> = {
service_areas: [nairobiServiceArea],
service_radius_km: 50
};
await supabase
.from('movers')
.update(moverUpdate)
.eq('id', moverId);import type { Point } from '@/types/database';
const currentLocation: Point = {
type: 'Point',
coordinates: [36.8500, -1.2800]
};
await supabase
.from('mover_locations')
.insert({
mover_id: moverId,
booking_id: bookingId,
location: currentLocation,
latitude: currentLocation.coordinates[1],
longitude: currentLocation.coordinates[0],
heading: 180,
speed: 45,
recorded_at: new Date().toISOString()
});import mapboxgl from 'mapbox-gl';
import type { Point } from '@/types/database';
function addMarker(map: mapboxgl.Map, location: Point, title: string) {
new mapboxgl.Marker()
.setLngLat(location.coordinates)
.setPopup(new mapboxgl.Popup().setText(title))
.addTo(map);
}
// Add pickup marker
addMarker(map, booking.pickup_location, 'Pickup Location');
// Add dropoff marker
addMarker(map, booking.dropoff_location, 'Dropoff Location');import type { Point, Polygon } from 'geojson';
function isValidPoint(value: any): value is Point {
return (
value &&
value.type === 'Point' &&
Array.isArray(value.coordinates) &&
value.coordinates.length === 2 &&
typeof value.coordinates[0] === 'number' &&
typeof value.coordinates[1] === 'number'
);
}
function isValidPolygon(value: any): value is Polygon {
return (
value &&
value.type === 'Polygon' &&
Array.isArray(value.coordinates) &&
value.coordinates.length > 0 &&
Array.isArray(value.coordinates[0])
);
}
// Usage
if (!isValidPoint(booking.pickup_location)) {
throw new Error('Invalid pickup location format');
}The database stores locations as PostGIS geography types, which Supabase automatically converts to GeoJSON:
SQL Query:
SELECT
id,
ST_AsGeoJSON(pickup_location)::json as pickup_location,
ST_AsGeoJSON(dropoff_location)::json as dropoff_location
FROM bookings;Supabase Client:
// Automatic conversion - no manual parsing needed
const { data } = await supabase
.from('bookings')
.select('id, pickup_location, dropoff_location')
.single();
// data.pickup_location is already a Point type
const [lng, lat] = data.pickup_location.coordinates;Supabase automatically converts GeoJSON to PostGIS:
const location: Point = {
type: 'Point',
coordinates: [36.8219, -1.2921]
};
// Supabase handles conversion automatically
await supabase
.from('bookings')
.insert({
pickup_location: location,
// ... other fields
});Code that was using any types will continue to work, but now benefits from type checking:
Before (no type safety):
const location = booking.pickup_location;
const lat = location.coordinates[1]; // No autocomplete, no validationAfter (type-safe):
const location: Point = booking.pickup_location;
const [lng, lat] = location.coordinates; // Autocomplete and validationNone - This is purely additive type safety. The runtime behavior remains unchanged.
If existing code was incorrectly using location fields (e.g., { lat, lng } instead of GeoJSON), TypeScript will now catch these errors at compile time.
import { describe, it, expect } from 'vitest';
import type { Point, Polygon } from 'geojson';
describe('GeoJSON Type Validation', () => {
it('should accept valid Point geometry', () => {
const point: Point = {
type: 'Point',
coordinates: [36.8219, -1.2921]
};
expect(point.type).toBe('Point');
expect(point.coordinates).toHaveLength(2);
});
it('should accept valid Polygon geometry', () => {
const polygon: Polygon = {
type: 'Polygon',
coordinates: [[
[36.7, -1.3],
[36.9, -1.3],
[36.9, -1.1],
[36.7, -1.1],
[36.7, -1.3]
]]
};
expect(polygon.type).toBe('Polygon');
expect(polygon.coordinates[0]).toHaveLength(5);
});
});- Type Definitions:
src/types/database.ts - Database Migrations:
supabase/migrations/20251008_part2_marketplace_schema.sql
- Usage Examples:
src/components/LocationAutocomplete.tsxsrc/pages/MoverDashboard.tsxsrc/services/api.ts
- RFC 7946: The GeoJSON Format
- PostGIS GeoJSON Documentation
- Mapbox GL JS GeoJSON Source
- @types/geojson NPM Package
Consider adding helper functions for common GeoJSON operations:
- Distance calculation between Points
- Point-in-Polygon checks for service area validation
- Bounding box generation
- Coordinate transformation utilities