Maps in TypeScript: Building a Waze‑Style Real‑Time Navigation App
mappingrealtimegeospatial

Maps in TypeScript: Building a Waze‑Style Real‑Time Navigation App

ttypescript
2026-01-25 12:00:00
11 min read
Advertisement

Learn to build a Waze‑style real‑time mapping app in TypeScript: typed websockets, geospatial types, predictive routing, and React/Next.js patterns.

Build a Waze‑Style Real‑Time Map in TypeScript — fast, typed, and event-driven

If you've ever tried to glue together maps, live vehicle telemetry, and user‑reported incidents in JavaScript, you know how quickly types, events, and edge cases explode. In 2026 the problem is the same — but the tooling is better. This guide uses the Google Maps vs Waze split (centralized routing vs crowdsourced incidents) as a lens to design a robust, scalable, and strongly typed mapping app with TypeScript across React, Vue, Node, and Next.js.

What you'll get immediately

  • Architecture patterns for event‑driven mapping apps
  • Typed WebSocket and WebTransport message contracts
  • Practical geospatial types and PostGIS queries in TypeScript
  • Simple predictive routing (ETA + interpolation) with code
  • Client patterns in React, Vue, and Next.js with optimistic user reports

Why the Google Maps vs Waze rivalry helps you design better systems

Google Maps focuses on authoritative map/routing data and global coverage; Waze focuses on real‑time, user‑contributed events (incidents, jams) that change routing. Combining both approaches gives you the best of both worlds: trusted base maps and a fast incident stream that improves routing in realtime.

Architecturally that means separating responsibilities and data sources: base routing graph and real‑time event layer. The former is relatively stable and often persisted in PostGIS or on cloud vector tiles; the latter is highly dynamic and best modeled as an event stream (WebSockets / WebTransport / server events) with typed messages.

Core architecture — event layer + routing engine

  1. Client map (React/Vue) renders a vector tile base and subscribes to a real‑time event stream for incidents and telemetry.
  2. Backend streaming layer ingests vehicle telemetry and user incident reports, validates them, and broadcasts typed messages.
  3. Routing engine computes ETA using base graph + live updates, exposing route updates to clients.
  4. Persistence: PostGIS for geospatial queries, Redis or tile cache for hotspot lookups, and an analytics store for ML models.
  • WebTransport and WebSockets: WebTransport is gaining adoption for low‑latency, multiplexed datagrams — consider it for high‑scale fleets (late 2025 saw broader support in browsers and cloud infra).
  • Edge SQL and vector tiles: More teams run spatial filters at the edge (RDS Proxy, managed PostGIS, or Edge SQL) to reduce latency for nearby incident lookups.
  • Strong runtime validation: Use zod/io‑ts for runtime validation of inbound messages to avoid trust issues from client reports.

Typed real‑time protocol — messages you can trust

Define a compact, typed message contract for telemetry and incidents. We use zod for runtime validation but keep TypeScript types first class.

// types/protocol.ts
import { z } from 'zod'

export const GeoPoint = z.object({ lat: z.number(), lng: z.number() })
export type GeoPoint = z.infer

export const VehicleTelemetry = z.object({
  vehicleId: z.string(),
  position: GeoPoint,
  speedKph: z.number(),
  heading: z.number(),
  timestamp: z.number() // ms since epoch
})
export type VehicleTelemetry = z.infer

export const IncidentReport = z.object({
  id: z.string(),
  reporterId: z.string(),
  type: z.enum(['accident','hazard','road_closed','police','traffic']),
  location: GeoPoint,
  severity: z.number().min(1).max(5),
  description: z.string().optional(),
  timestamp: z.number()
})
export type IncidentReport = z.infer

export const ServerMessage = z.discriminatedUnion('type', [
  z.object({ type: z.literal('telemetry'), payload: VehicleTelemetry }),
  z.object({ type: z.literal('incident'), payload: IncidentReport }),
  z.object({ type: z.literal('route_update'), payload: z.any() })
])
export type ServerMessage = z.infer

Runtime validation prevents malformed client data from poisoning routing. This is the exact benefit Waze needed: trust through validation, not blind acceptance.

Node server: typed WebSocket (ws) example

This simple Node snippet shows validating incoming incident reports and broadcasting to connected clients.

// server/ws.ts
import WebSocket, { WebSocketServer } from 'ws'
import { IncidentReport, ServerMessage, ServerMessage as ServerMsgSchema } from './types/protocol'
import { z } from 'zod'

const wss = new WebSocketServer({ port: 8080 })

wss.on('connection', (ws) => {
  ws.on('message', (raw) => {
    try {
      const parsed = JSON.parse(raw.toString())
      // Simple discriminated union guard
      if (parsed?.type === 'incident') {
        const validated = IncidentReport.parse(parsed.payload)
        // persist, moderate, then broadcast
        broadcast({ type: 'incident', payload: validated })
      }
    } catch (err) {
      console.warn('invalid message', err)
    }
  })
})

function broadcast(msg: unknown) {
  const ok = ServerMsgSchema.safeParse(msg)
  if (!ok.success) return
  const s = JSON.stringify(msg)
  for (const c of wss.clients) if (c.readyState === WebSocket.OPEN) c.send(s)
}

Client integration (React) — typed subscription

In React you want a small hook to manage the connection and deliver typed messages. This pattern is framework‑agnostic and easy to adapt to Vue or Svelte.

// web/hooks/useRealtime.ts
import { useEffect, useRef, useState } from 'react'
import { ServerMessage } from '../../types/protocol'

export function useRealtime(url: string) {
  const [messages, setMessages] = useState([])
  const wsRef = useRef()

  useEffect(() => {
    const ws = new WebSocket(url)
    wsRef.current = ws
    ws.onmessage = (ev) => {
      try {
        const parsed = JSON.parse(ev.data) as ServerMessage
        setMessages((m) => [...m.slice(-199), parsed]) // keep last 200
      } catch {}
    }
    return () => ws.close()
  }, [url])

  return { messages, send: (m: any) => wsRef.current?.send(JSON.stringify(m)) }
}

React map example (concept)

Use React + Google Maps or react‑leaflet. When an incident message arrives, add a marker layer and optionally re‑route if on the path.

Geospatial types and queries — PostGIS + TypeScript

For spatial queries use PostGIS and keep TypeScript types for DB rows. Your server will often need "nearby incidents" queries.

// server/db.ts (node-postgres)
import { Pool } from 'pg'
export const pool = new Pool({ connectionString: process.env.DATABASE_URL })

// Query: find incidents within radius (meters)
export async function findIncidentsNear(lat: number, lng: number, radiusMeters = 1000) {
  const sql = `
    SELECT id, reporter_id, type, severity, description, ST_AsGeoJSON(location) AS location, timestamp
    FROM incidents
    WHERE ST_DWithin(location::geography, ST_MakePoint($1,$2)::geography, $3)
    ORDER BY timestamp DESC
    LIMIT 200
  `
  const { rows } = await pool.query(sql, [lng, lat, radiusMeters])
  return rows.map(r => ({ ...r, location: JSON.parse(r.location) }))
}

Use typed mappers or edge ORMs (Prisma with native PostGIS types is improving; in 2026 Prisma has better spatial support) to keep types end‑to‑end.

Predictive routing & ETA — pragmatic, explainable model

High‑accuracy predictive routing can become complex fast (graph algorithms + ML). For many apps a hybrid approach works: historical baseline + live delta + incident impact weight.

Formula (simplified):

ETA = baseline_travel_time * (1 + live_speed_delta_weight * live_delta + incident_impact)

Where:

  • baseline_travel_time is from historical speeds on the routing graph
  • live_delta is the proportional change of current speed vs historical
  • incident_impact is additive time penalty derived from nearby incident severity and distance
// server/predict.ts
export function predictETA(baseSeconds: number, historicalSpeed: number, liveSpeed: number, nearbyIncidents: {severity:number,distanceMeters:number}[]) {
  const liveDelta = historicalSpeed > 0 ? (historicalSpeed - liveSpeed) / historicalSpeed : 0
  const liveWeight = 0.9 // tuneable
  const incidentImpact = nearbyIncidents.reduce((acc, inc) => acc + (inc.severity * Math.exp(-inc.distanceMeters/500) * 15), 0)
  const eta = baseSeconds * (1 + liveWeight * liveDelta) + incidentImpact
  return Math.round(eta)
}

This gives you a robust, explainable ETA and is cheap. For advanced accuracy, feed telemetry into an ML model at the edge and deploy model inference at the edge.

Client‑reported incidents — UX, validation, and moderation

Waze's advantage is real people reporting things. But that requires trust controls: validation, spam controls, and community moderation. Architect the flow like this:

  1. Client submits incident via typed API or WebSocket. Validate using zod.
  2. Store incident in DB with state 'pending'. Quickly surface to nearby users as "user report (unverified)".
  3. Aggregate corroborating reports (same area/type) to auto‑promote to 'confirmed'.
  4. Optional human or ML moderation for certain categories (police, road closed).
// api/report (Next.js / pages or app router)
import { z } from 'zod'
import { IncidentReport } from '../../types/protocol'

export default async function handler(req, res) {
  try {
    const payload = IncidentReport.parse(req.body)
    // persist with state=pending
    // notify nearby clients via WebSocket
    return res.status(201).json({ ok: true })
  } catch (e) {
    return res.status(400).json({ error: 'invalid report' })
  }
}

Framework integrations: four practical patterns

React (SPA) — hooks + optimistic updates

  • Use the useRealtime hook shown earlier.
  • When user reports an incident, immediately show a local marker (optimistic) and disable spamming via rate limits.
  • Integrate Google Maps/Mapbox with typed callback props (TypeScript types for map layers).

Next.js — server functions + edge

  • Expose incident lookup via edge function for low latency (Next.js edge runtime), returning typed JSON.
  • Use tRPC or typed REST with zod to keep types consistent between client and server.

Vue — composables for WebSocket

Create a composable similar to the React hook. Vue's reactivity makes layer updates trivial; keep types on the composable return values.

Node — typed services and validation

  • Keep validation at the perimeter (WebSocket server / HTTP endpoints).
  • Use event buses (NATS, Kafka) to scale telemetry ingestion; messages should use the same zod schemas serialized to JSON.

Scaling concerns & edge strategies

  • Partition by geographic tile: broadcast events only to clients in relevant tiles. Use vector tile indices or H3 indexes in 2026 — H3 is widely used for spatial partitioning.
  • Use WebTransport or UDP datagrams for high throughput telemetry (fleet vehicles) and reserve WebSockets for control and incident broadcasts.
  • Cache nearby incident lookups at the edge and invalidate intelligently when new incidents arrive.

Testing and observability

  • Unit test serialization/parsing of your zod schemas — these are security boundaries.
  • Load test your WebSocket event fan‑out and partitioning strategy (k6, Artillery with WebSocket modules).
  • Instrument routing deltas: track how many routes changed due to incidents vs baseline.

Real world example: flow from driver report to re‑routing

  1. Driver reports a road blockage (client sends typed incident report via WebSocket).
  2. Server validates and persists incident; initial state = pending.
  3. Server broadcasts incident to clients in nearby tiles (optimistic UI shows 'reported nearby').
  4. Routing engine adjusts ETA: finds route segments intersecting incident geometry, applies incident penalty (predictETA), emits route_update.
  5. Clients on the route receive route_update and optionally present a reroute prompt; drivers accept and receive new path.
Design principle: validate early, broadcast selectively, and keep the routing graph authoritative while letting incident streams nudge ETA and paths.

Actionable checklist to ship a prototype in a week

  1. Day 1: define zod schemas for telemetry, incidents, route updates.
  2. Day 2: Node WebSocket server that ingests telemetry and incident reports (persist to PostGIS).
  3. Day 3: Next.js API to query nearby incidents (edge function) and a simple React client that shows markers.
  4. Day 4: Implement predictETA and a basic re‑route engine that invalidates routes intersecting incidents.
  5. Day 5: Add moderation workflow and corroboration logic; add basic authentication for reporters.
  6. Day 6–7: Load test and iterate on partitioning and caching strategies (H3 tiling, edge SQL).

Advanced strategies & future directions

  • Deploy ML models at the edge for real‑time ETA corrections using telemetry aggregates.
  • Explore WebTransport datagram channels for lower latency fleet telemetry.
  • Consider vector tile patches for incident highlights so clients can render overlays without extra HTTP requests.
  • Use federated moderation and reputation scoring to reduce spam and abuse in user reports.

Final takeaways

  • Type everything at the boundary (zod + TypeScript gives runtime safety and developer ergonomics).
  • Separate base maps from the event layer — keep routing authoritative, events ephemeral but influential.
  • Optimize partitioning (H3 / vector tiles) to reduce broadcast blast radius.
  • Start simple with predictETA and add models later — explainability wins for driver trust.

The Waze vs Google Maps rivalry isn't a fight; it's complementary lessons. Use Google Maps' stable routing data and Waze's crowd intelligence pattern to build a TypeScript system that is fast, typed, and resilient.

Next steps (call to action)

Ready to prototype? Clone the starter repo (TypeScript + zod + PostGIS + Next.js) and run through the 7‑day checklist above. If you want a curated checklist or starter template tailored to your stack (React, Vue, or full Next.js + Edge), request the repo and a 30‑minute walkthrough.

Ship safer, faster, and with confidence — start your realtime TypeScript map today.

Advertisement

Related Topics

#mapping#realtime#geospatial
t

typescript

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-01-24T03:55:26.505Z