7. Chapter 7: TypeScript for Web GIS#

7.1. Learning Objectives#

By the end of this chapter, you will understand:

  • Benefits of TypeScript for Web GIS development

  • Type annotations and interfaces for geospatial data

  • Advanced TypeScript features for mapping applications

  • Integration with popular mapping libraries

7.2. Why Use TypeScript?#

TypeScript adds static type checking to JavaScript, bringing significant benefits to Web GIS development where data integrity and API correctness are crucial.

7.2.1. Benefits for Web GIS#

Type Safety for Geospatial Data:

// Basic types for coordinates
type Coordinate = [number, number]; // [lng, lat]
type BoundingBox = [number, number, number, number]; // [west, south, east, north]

// TypeScript catches errors at compile time
const validCoord: Coordinate = [-74.006, 40.7128];
const invalidCoord: Coordinate = ["lat", "lng"]; // ❌ Error!

API Contract Enforcement:

// Simple map configuration interface
interface MapConfig {
  container: string;
  center: Coordinate;
  zoom: number;
  style: string;
}

function initializeMap(config: MapConfig) {
  return new maplibregl.Map(config);
}

Enhanced Developer Experience:

  • IntelliSense and autocomplete

  • Refactoring support

  • Early error detection

  • Better documentation through types

7.3. Type Annotations and Interfaces#

7.3.1. Basic Types for Geospatial Data#

// Basic type annotations
let mapZoom: number = 12;
let layerName: string = 'restaurants';
let isVisible: boolean = true;

// Common GIS types
type Point = [number, number];
type BBox = [number, number, number, number];
type ZoomLevel = number;

// Function types
type ClickHandler = (event: any) => void;
type DataFilter = (features: any[]) => any[];

7.3.2. GeoJSON Type Definitions#

// Complete GeoJSON type system
interface GeoJSONGeometry {
  type:
    | "Point"
    | "LineString"
    | "Polygon"
    | "MultiPoint"
    | "MultiLineString"
    | "MultiPolygon";
  coordinates: number[] | number[][] | number[][][];
}

interface GeoJSONFeature {
  type: "Feature";
  geometry: GeoJSONGeometry;
  properties: Record<string, any>;
  id?: string | number;
}

interface GeoJSONFeatureCollection {
  type: "FeatureCollection";
  features: GeoJSONFeature[];
}

// Specific geometry types
interface PointGeometry {
  type: "Point";
  coordinates: [number, number];
}

interface PolygonGeometry {
  type: "Polygon";
  coordinates: number[][][];
}

// Specific feature types
interface RestaurantFeature {
  type: "Feature";
  geometry: {
    type: "Point";
    coordinates: [number, number];
  };
  properties: {
    name: string;
    cuisine: string;
    rating: number;
  };
}

7.3.3. Map Library Type Integration#

// Simple map wrapper
import maplibregl from "maplibre-gl";

class TypedMap {
  private map: maplibregl.Map;

  constructor(container: string, center: [number, number], zoom: number) {
    this.map = new maplibregl.Map({
      container,
      style: 'https://demotiles.maplibre.org/style.json',
      center,
      zoom
    });
  }

  addGeoJSONLayer(id: string, data: GeoJSONFeatureCollection): void {
    this.map.addSource(id, {
      type: 'geojson',
      data
    });
    
    this.map.addLayer({
      id,
      type: 'circle',
      source: id
    });
  }
}

7.4. Advanced TypeScript Features#

7.4.1. Generics for Reusable Components#

// Generic data loader
class DataLoader<T extends GeoJSONFeature> {
  private cache = new Map<string, T[]>();

  async load(url: string, validator: (data: any) => data is T[]): Promise<T[]> {
    if (this.cache.has(url)) {
      return this.cache.get(url)!;
    }

    const response = await fetch(url);
    const json = await response.json();

    if (!validator(json.features)) {
      throw new Error("Invalid data format");
    }

    this.cache.set(url, json.features);
    return json.features;
  }

  clearCache(): void {
    this.cache.clear();
  }
}

// Type guards for validation
function isRestaurantFeature(feature: any): feature is RestaurantFeature {
  return (
    feature &&
    feature.type === "Feature" &&
    feature.geometry?.type === "Point" &&
    typeof feature.properties?.name === "string" &&
    typeof feature.properties?.cuisine === "string" &&
    typeof feature.properties?.rating === "number"
  );
}

function isRestaurantFeatureArray(data: any): data is RestaurantFeature[] {
  return Array.isArray(data) && data.every(isRestaurantFeature);
}

// Usage
const restaurantLoader = new DataLoader<RestaurantFeature>();
const restaurants = await restaurantLoader.load(
  "/api/restaurants.json",
  isRestaurantFeatureArray
);

7.4.2. Utility Types for Map Configuration#

// Extract and modify types
type RestaurantProperties = RestaurantFeature["properties"];
type RestaurantUpdate = Partial<RestaurantProperties>;

// Pick specific properties
type RestaurantSummary = Pick<RestaurantProperties, "name" | "cuisine" | "rating">;

// Omit sensitive properties
type PublicRestaurantData = Omit<RestaurantProperties, "phone">;

// Simple layer configuration
interface LayerConfig {
  id: string;
  type: "circle" | "fill" | "line" | "symbol";
  source: string;
  paint: Record<string, any>;
}

// Map style configuration
interface MapStyleConfig {
  type: "vector" | "raster" | "geojson";
  url?: string;
  tiles?: string[];
  data?: GeoJSONFeatureCollection;
}

7.4.3. Event System with Types#

// Type-safe event system
interface MapEventMap {
  click: MapMouseEvent;
  featureselect: FeatureSelectEvent;
  layertoggle: LayerToggleEvent;
}

interface FeatureSelectEvent {
  feature: GeoJSONFeature;
  point: [number, number];
}

interface LayerToggleEvent {
  layerId: string;
  visible: boolean;
}

// Simple typed event emitter
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners = new Map<keyof T, Function[]>();

  on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const callbacks = this.listeners.get(event) || [];
    callbacks.forEach(callback => callback(data));
  }
}

// Usage with type safety
const mapEvents = new TypedEventEmitter<MapEventMap>();

mapEvents.on("featureselect", (event) => {
  console.log(`Selected: ${event.feature.properties?.name}`);
});

7.5. Strict Typing in Geospatial Applications#

7.5.1. Configuration Validation#

// Strict configuration interfaces
interface MapConfiguration {
  readonly container: string;
  readonly initialView: {
    center: Coordinate;
    zoom: number;
    bounds?: BoundingBox;
  };
  readonly style: StyleConfiguration;
  readonly controls: ControlsConfiguration;
  readonly layers: LayerConfiguration[];
}

interface StyleConfiguration {
  readonly type: "vector" | "raster" | "custom";
  readonly url?: string;
  readonly specification?: StyleSpecification;
}

interface ControlsConfiguration {
  readonly navigation: boolean;
  readonly scale: boolean;
  readonly fullscreen: boolean;
  readonly geolocate: boolean;
  readonly attribution: boolean | AttributionOptions;
}

interface AttributionOptions {
  readonly compact: boolean;
  readonly customAttribution?: string[];
}

// Configuration validation with branded types
type ValidatedConfig = MapConfiguration & { readonly __validated: true };

function validateMapConfiguration(config: MapConfiguration): ValidatedConfig {
  // Validation logic
  if (!config.container) {
    throw new Error("Container is required");
  }

  if (!isValidCoordinate(config.initialView.center)) {
    throw new Error("Invalid center coordinate");
  }

  if (config.initialView.zoom < 0 || config.initialView.zoom > 22) {
    throw new Error("Zoom level must be between 0 and 22");
  }

  return config as ValidatedConfig;
}

function isValidCoordinate(coord: Coordinate): boolean {
  const [lng, lat] = coord;
  return lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90;
}

// Only accept validated configurations
function createMap(config: ValidatedConfig): TypedMap {
  return new TypedMap(config);
}

7.5.2. Data Transformation Pipelines#

// Type-safe data processing pipeline
interface DataPipeline<TInput, TOutput> {
  process(input: TInput): TOutput;
}

class FeatureProcessor<TIn extends GeoJSONFeature, TOut extends GeoJSONFeature>
  implements DataPipeline<TIn[], TOut[]>
{
  private transformers: Array<(feature: TIn) => TOut> = [];
  private filters: Array<(feature: TIn) => boolean> = [];

  addTransformer(transformer: (feature: TIn) => TOut): this {
    this.transformers.push(transformer);
    return this;
  }

  addFilter(filter: (feature: TIn) => boolean): this {
    this.filters.push(filter);
    return this;
  }

  process(features: TIn[]): TOut[] {
    return features
      .filter((feature) => this.filters.every((filter) => filter(feature)))
      .map((feature) =>
        this.transformers.reduce(
          (acc, transformer) => transformer(acc as TIn),
          feature
        )
      ) as TOut[];
  }
}

// Usage example
const restaurantProcessor = new FeatureProcessor<
  RestaurantFeature,
  RestaurantFeature
>()
  .addFilter((restaurant) => restaurant.properties.rating >= 4.0)
  .addTransformer((restaurant) => ({
    ...restaurant,
    properties: {
      ...restaurant.properties,
      category: "high-rated",
    },
  }));

const highRatedRestaurants = restaurantProcessor.process(allRestaurants);

7.5.3. Error Handling with Types#

// Result type for error handling
type Result<T, E = Error> = Success<T> | Failure<E>;

interface Success<T> {
  readonly success: true;
  readonly data: T;
}

interface Failure<E> {
  readonly success: false;
  readonly error: E;
}

// Helper functions
function success<T>(data: T): Success<T> {
  return { success: true, data };
}

function failure<E>(error: E): Failure<E> {
  return { success: false, error };
}

// Async operations with Result types
async function loadGeoJSONSafely(
  url: string
): Promise<Result<GeoJSONFeatureCollection>> {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      return failure(
        new Error(`HTTP ${response.status}: ${response.statusText}`)
      );
    }

    const data = await response.json();

    if (!isValidGeoJSON(data)) {
      return failure(new Error("Invalid GeoJSON format"));
    }

    return success(data);
  } catch (error) {
    return failure(error as Error);
  }
}

function isValidGeoJSON(data: any): data is GeoJSONFeatureCollection {
  return (
    data && data.type === "FeatureCollection" && Array.isArray(data.features)
  );
}

// Usage with proper error handling
async function initializeMapLayer(url: string) {
  const result = await loadGeoJSONSafely(url);

  if (result.success) {
    // TypeScript knows result.data is GeoJSONFeatureCollection
    map.addSource("data", {
      type: "geojson",
      data: result.data,
    });
  } else {
    // TypeScript knows result.error is Error
    console.error("Failed to load data:", result.error.message);
    showErrorNotification(result.error.message);
  }
}

7.6. Integration with Mapping Libraries#

7.6.1. MapLibre GL JS Type Definitions#

// Extended MapLibre types for better integration
declare module "maplibre-gl" {
  interface Map {
    addTypedLayer<T extends GeoJSONFeature>(
      id: string,
      data: T[],
      style: LayerSpecification
    ): void;

    queryTypedFeatures<T extends GeoJSONFeature>(
      pointOrBox: PointLike | [PointLike, PointLike],
      options?: QueryRenderedFeaturesOptions
    ): T[];
  }
}

// Implementation of extended methods
Map.prototype.addTypedLayer = function <T extends GeoJSONFeature>(
  id: string,
  data: T[],
  style: LayerSpecification
) {
  this.addSource(id, {
    type: "geojson",
    data: {
      type: "FeatureCollection",
      features: data,
    },
  });

  this.addLayer({
    id,
    source: id,
    ...style,
  });
};

Map.prototype.queryTypedFeatures = function <T extends GeoJSONFeature>(
  pointOrBox: PointLike | [PointLike, PointLike],
  options?: QueryRenderedFeaturesOptions
): T[] {
  return this.queryRenderedFeatures(pointOrBox, options) as T[];
};

7.6.2. React Integration with TypeScript#

// Typed React hooks for map interactions
import { useEffect, useRef, useState, useCallback } from "react";
import maplibregl from "maplibre-gl";

interface UseMapOptions {
  container: string;
  style: string;
  center: Coordinate;
  zoom: number;
}

interface UseMapReturn {
  map: maplibregl.Map | null;
  isLoaded: boolean;
  error: Error | null;
}

export function useMap(options: UseMapOptions): UseMapReturn {
  const mapRef = useRef<maplibregl.Map | null>(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    try {
      const map = new maplibregl.Map({
        container: options.container,
        style: options.style,
        center: options.center,
        zoom: options.zoom,
      });

      map.on("load", () => setIsLoaded(true));
      map.on("error", (e) => setError(e.error));

      mapRef.current = map;

      return () => {
        map.remove();
        mapRef.current = null;
        setIsLoaded(false);
        setError(null);
      };
    } catch (e) {
      setError(e as Error);
    }
  }, [options.container, options.style]);

  return {
    map: mapRef.current,
    isLoaded,
    error,
  };
}

// Typed map component
interface MapComponentProps {
  config: MapConfiguration;
  onFeatureClick?: (feature: GeoJSONFeature) => void;
  className?: string;
}

export const MapComponent: React.FC<MapComponentProps> = ({
  config,
  onFeatureClick,
  className = "",
}) => {
  const mapContainerId = "map-" + useId();
  const { map, isLoaded, error } = useMap({
    container: mapContainerId,
    style: config.style.url || "",
    center: config.initialView.center,
    zoom: config.initialView.zoom,
  });

  useEffect(() => {
    if (!map || !isLoaded || !onFeatureClick) return;

    const handleClick = (e: maplibregl.MapMouseEvent) => {
      const features = map.queryRenderedFeatures(e.point);
      if (features.length > 0) {
        onFeatureClick(features[0]);
      }
    };

    map.on("click", handleClick);
    return () => map.off("click", handleClick);
  }, [map, isLoaded, onFeatureClick]);

  if (error) {
    return <div className="map-error">Error loading map: {error.message}</div>;
  }

  return (
    <div
      id={mapContainerId}
      className={`map-container ${className}`}
      style={{ width: "100%", height: "100%" }}
    />
  );
};

7.7. Summary#

TypeScript significantly improves Web GIS development by providing type safety, better tooling support, and enhanced code documentation. Strong typing helps prevent common errors when working with geospatial data, ensures API correctness, and improves the overall developer experience. The investment in learning TypeScript pays dividends in larger, more complex mapping applications.

The next chapter will introduce MapLibre GL JS and demonstrate how to create interactive web maps using this powerful library.

7.8. Exercises#

7.8.1. Exercise 7.1: TypeScript Configuration and Setup#

Objective: Configure TypeScript for a Web GIS project with appropriate compiler options and tooling.

Instructions:

  1. Set up TypeScript configuration:

    • Create a comprehensive tsconfig.json for Web GIS development

    • Configure strict type checking options

    • Set up path mapping for clean imports

    • Configure module resolution for mapping libraries

  2. Integrate with build tools:

    • Configure Vite to work with TypeScript

    • Set up ESLint with TypeScript rules

    • Configure Prettier for consistent formatting

  3. Test the setup:

    • Create a simple TypeScript file that imports mapping libraries

    • Verify type checking works correctly

    • Test the development and build processes

Deliverable: A properly configured TypeScript development environment.

7.8.2. Exercise 7.2: GeoJSON Type Definitions#

Objective: Create comprehensive type definitions for geospatial data structures.

Instructions:

  1. Define complete GeoJSON types:

    • All geometry types (Point, LineString, Polygon, etc.)

    • Feature and FeatureCollection interfaces

    • Coordinate and bounding box types

  2. Create domain-specific feature types:

    • Restaurant features with typed properties

    • Building features with 3D information

    • Transportation route features

    • Administrative boundary features

  3. Implement type guards and validation:

    • Runtime type checking functions

    • Data validation with helpful error messages

    • Type narrowing for different feature types

Deliverable: A comprehensive GeoJSON type library with validation functions.

7.8.3. Exercise 7.3: Mapping Library Integration#

Objective: Create type-safe wrappers for mapping library APIs.

Instructions:

  1. Create a typed wrapper for MapLibre GL JS:

    • Type-safe map configuration

    • Strongly typed event handlers

    • Generic layer and source management

  2. Implement type-safe data loading:

    • Generic data loader with type constraints

    • Compile-time validation of data structures

    • Type-safe error handling

  3. Create typed map controls:

    • Custom control interfaces

    • Type-safe event emission and handling

    • Strongly typed configuration options

Deliverable: Type-safe mapping library wrapper with comprehensive documentation.

7.8.4. Exercise 7.4: Advanced TypeScript Features#

Objective: Utilize advanced TypeScript features for complex Web GIS scenarios.

Instructions:

  1. Implement generic spatial data processing:

    • Generic functions that work with different feature types

    • Conditional types for different processing modes

    • Mapped types for data transformations

  2. Create a type-safe configuration system:

    • Use template literal types for style configurations

    • Implement discriminated unions for different map modes

    • Create recursive types for nested configurations

  3. Build a type-safe plugin system:

    • Define plugin interfaces with strict contracts

    • Use module augmentation for extending base types

    • Implement type-safe dependency injection

Deliverable: Advanced TypeScript implementation showcasing sophisticated type features.

7.8.5. Exercise 7.5: Error Handling with Types#

Objective: Implement robust error handling using TypeScript’s type system.

Instructions:

  1. Create a Result type system:

    • Success and Error union types

    • Helper functions for creating results

    • Type-safe error handling patterns

  2. Implement domain-specific errors:

    • Geospatial validation errors

    • Network and API errors

    • Map rendering errors

    • User input errors

  3. Build error recovery mechanisms:

    • Type-safe retry logic

    • Fallback data providers

    • Graceful degradation strategies

Deliverable: Comprehensive error handling system with strong type safety.

7.8.6. Exercise 7.6: Testing TypeScript Code#

Objective: Implement comprehensive testing for TypeScript Web GIS applications.

Instructions:

  1. Set up testing framework:

    • Configure Jest or Vitest with TypeScript

    • Set up test utilities for geospatial data

    • Create mock implementations for external APIs

  2. Write comprehensive tests:

    • Unit tests for utility functions

    • Integration tests for data processing

    • Type-level tests to verify type correctness

  3. Test error scenarios:

    • Invalid data handling

    • Network failure scenarios

    • Edge cases in spatial calculations

Deliverable: Well-tested TypeScript codebase with high coverage and type safety verification.

7.8.7. Exercise 7.7: Performance Optimization with Types#

Objective: Use TypeScript’s type system to guide performance optimizations.

Instructions:

  1. Implement type-guided optimizations:

    • Use readonly types to enable compiler optimizations

    • Implement branded types for performance-critical code paths

    • Create types that enforce efficient data structures

  2. Build performance monitoring:

    • Type-safe performance measurement utilities

    • Compile-time performance hints

    • Runtime performance validation

  3. Optimize based on type information:

    • Use discriminated unions to eliminate runtime checks

    • Implement zero-cost abstractions

    • Create performance-oriented API designs

Deliverable: Performance-optimized TypeScript code with measurable improvements.

7.8.8. Exercise 7.8: Migration Project#

Objective: Migrate an existing JavaScript Web GIS project to TypeScript.

Instructions:

  1. Plan the migration strategy:

    • Analyze existing codebase for type-related issues

    • Create migration timeline and phases

    • Identify high-risk areas requiring careful attention

  2. Execute gradual migration:

    • Start with configuration files and build setup

    • Migrate utility functions first

    • Gradually add types to components

    • Refactor problematic code patterns

  3. Validate improvements:

    • Measure reduction in runtime errors

    • Document developer experience improvements

    • Compare code quality metrics before and after

Deliverable: Successfully migrated TypeScript project with documented benefits and lessons learned.

Reflection Questions:

  • How does TypeScript change your approach to Web GIS development?

  • What are the most valuable TypeScript features for geospatial applications?

  • How do you balance type safety with development speed?

  • What strategies work best for introducing TypeScript to existing projects?

7.9. Further Reading#