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:
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
Integrate with build tools:
Configure Vite to work with TypeScript
Set up ESLint with TypeScript rules
Configure Prettier for consistent formatting
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:
Define complete GeoJSON types:
All geometry types (Point, LineString, Polygon, etc.)
Feature and FeatureCollection interfaces
Coordinate and bounding box types
Create domain-specific feature types:
Restaurant features with typed properties
Building features with 3D information
Transportation route features
Administrative boundary features
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:
Create a typed wrapper for MapLibre GL JS:
Type-safe map configuration
Strongly typed event handlers
Generic layer and source management
Implement type-safe data loading:
Generic data loader with type constraints
Compile-time validation of data structures
Type-safe error handling
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:
Implement generic spatial data processing:
Generic functions that work with different feature types
Conditional types for different processing modes
Mapped types for data transformations
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
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:
Create a Result type system:
Success and Error union types
Helper functions for creating results
Type-safe error handling patterns
Implement domain-specific errors:
Geospatial validation errors
Network and API errors
Map rendering errors
User input errors
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:
Set up testing framework:
Configure Jest or Vitest with TypeScript
Set up test utilities for geospatial data
Create mock implementations for external APIs
Write comprehensive tests:
Unit tests for utility functions
Integration tests for data processing
Type-level tests to verify type correctness
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:
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
Build performance monitoring:
Type-safe performance measurement utilities
Compile-time performance hints
Runtime performance validation
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:
Plan the migration strategy:
Analyze existing codebase for type-related issues
Create migration timeline and phases
Identify high-risk areas requiring careful attention
Execute gradual migration:
Start with configuration files and build setup
Migrate utility functions first
Gradually add types to components
Refactor problematic code patterns
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?