Создание типизированных оберток

Типизированные обертки над MapLibre GL JS позволяют превратить динамическое, слабо типизированное API в строго проверяемую систему, удобную для масштабных приложений. В основе подхода лежит идея изоляции «сырых» объектов библиотеки и предоставления поверх них слоя TypeScript-типов, контрактов и доменных интерфейсов, которые фиксируют структуру данных, событий и конфигурации карты.


JavaScript-API картографических движков традиционно построены вокруг гибких объектов: параметры слоёв, источников данных, событий и стилей принимаются в виде свободных структур. Это повышает скорость прототипирования, но снижает устойчивость системы при росте кода.

Типизированные обёртки решают несколько ключевых задач:

  • фиксация структуры конфигурации карты;
  • контроль корректности слоёв и источников на этапе компиляции;
  • унификация работы с событиями;
  • снижение количества runtime-ошибок при взаимодействии со стилями;
  • формирование доменной модели поверх низкоуровневого API.

Базовый слой абстракции над картой

Основной объект библиотеки — экземпляр карты. В типизированной архитектуре он никогда не используется напрямую. Вместо этого создаётся обёртка, описывающая допустимые операции.

import maplibregl, { Map } from "maplibre-gl";

export interface TypedMapOptions {
  container: string | HTMLElement;
  center: [number, number];
  zoom: number;
  style: string;
}

export class TypedMap {
  private map: Map;

  constructor(options: TypedMapOptions) {
    this.map = new maplibregl.Map(options);
  }

  getInstance(): Map {
    return this.map;
  }
}

Ключевой принцип — внешнему коду не предоставляется доступ к Map напрямую без необходимости. Это создаёт контролируемую точку интеграции.


Типизация конфигурации стиля

Стиль карты в MapLibre представляет собой сложную JSON-структуру. Без типизации она становится источником ошибок: опечатки в идентификаторах слоёв или источников не выявляются заранее.

Подход с обёртками включает:

  • выделение интерфейсов для источников;
  • строгое описание слоёв;
  • ограничение допустимых типов данных.
export type SourceId = string;
export type LayerId = string;

export interface TypedGeoJSONSource {
  type: "geojson";
  dat a: GeoJSON.FeatureCollection;
}

export interface TypedFillLayer {
  id: LayerId;
  type: "fill";
  source: SourceId;
  paint?: {
    "fill-color"?: string;
    "fill-opacity"?: number;
  };
}

Такой слой позволяет проверять корректность структуры ещё до отправки данных в карту.


Обобщённая модель источников данных

Источники данных в картографических приложениях обладают разной структурой: vector tiles, raster tiles, geojson. Унификация через generics позволяет формализовать зависимости.

interface BaseSource<TType extends string> {
  type: TType;
}

interface GeoJSONSource extends BaseSource<"geojson"> {
  data: GeoJSON.FeatureCollection;
}

interface RasterSource extends BaseSource<"raster"> {
  tiles: string[];
  tileSize?: number;
}

type AnySource = GeoJSONSource | RasterSource;

Дальнейшее расширение API карты строится на этих типах, что исключает передачу несовместимых данных.


Типизированное добавление слоёв

Добавление слоёв через обёртку позволяет контролировать соответствие слоя источнику данных.

type Layer =
  | TypedFillLayer
  | TypedLineLayer
  | TypedCircleLayer;

export class LayerManager {
  constructor(private map: Map) {}

  addLayer(layer: Layer) {
    this.map.addLayer(layer as any);
  }

  removeLayer(id: string) {
    this.map.removeLayer(id);
  }
}

На уровне TypeScript возможно расширение проверки: сопоставление layer.source с зарегистрированными источниками карты через дополнительный слой состояния.


Типизация событий карты

События в MapLibre представляют собой слабоструктурированные объекты. Типизированная обёртка вводит строгую сигнатуру обработчиков.

export interface MapEvents {
  click: maplibregl.MapMouseEvent;
  move: maplibregl.MapEvent;
  load: maplibregl.MapEvent;
}

export class TypedEventEmitter {
  constructor(private map: Map) {}

  on<K extends keyof MapEvents>(
    event: K,
    handler: (e: MapEvents[K]) => void
  ) {
    this.map.on(event, handler as any);
  }
}

Такой подход исключает передачу обработчика с неверным типом аргумента.


Модульное расширение типов (Module Augmentation)

При расширении функциональности карты часто требуется добавление пользовательских слоёв, источников или событий. TypeScript позволяет расширять типы библиотеки без модификации исходного кода.

declare module "maplibre-gl" {
  interface Map {
    _customRegistry?: Record<string, unknown>;
  }
}

Это создаёт механизм безопасного хранения метаданных внутри экземпляра карты.


Фабрика карт с типизированными параметрами

Фабричный подход позволяет централизовать создание экземпляров с единым контрактом конфигурации.

export interface MapFactoryOptions {
  container: HTMLElement;
  center: [number, number];
  zoom: number;
  style: string;
}

export function createTypedMap(options: MapFactoryOptions) {
  return new TypedMap(options);
}

Расширение фабрики может включать:

  • валидацию параметров;
  • нормализацию координат;
  • подключение стандартных слоёв;
  • регистрацию источников по умолчанию.

Интеграция с GeoJSON типами

Геоданные требуют строгого соответствия спецификации. TypeScript предоставляет типы GeoJSON, которые используются как основа доменной модели.

import { FeatureCollection, Geometry } from "geojson";

export interface TypedFeature<G extends Geometry> {
  type: "Feature";
  geometry: G;
  properties: Record<string, unknown>;
}

Такой уровень абстракции позволяет строить геопространственные операции без потери типовой безопасности.


Доменная модель поверх карты

Типизированная обёртка перестаёт быть просто оболочкой над API и превращается в доменный слой:

  • Map → MapContext
  • Layer → RenderLayer
  • Source → DataSource
  • Event → MapInteraction

Пример структуры:

export class MapContext {
  constructor(
    public map: TypedMap,
    public layers: LayerManager,
    public events: TypedEventEmitter
  ) {}
}

Такой подход позволяет отделить бизнес-логику от низкоуровневого взаимодействия с картой.


Строгая изоляция MapLibre API

Ключевой архитектурный принцип — запрет прямого доступа к нативному API вне обёрток. Это достигается:

  • приватизацией экземпляра карты;
  • экспозицией ограниченного набора методов;
  • контролем всех побочных эффектов через сервисный слой.
class SafeMap {
  private readonly map: Map;

  constructor(map: Map) {
    this.map = map;
  }

  setCenter(center: [number, number]) {
    this.map.setCenter(center);
  }
}

Типизированные плагины

Расширение функциональности карты через плагины требует строгого описания интерфейсов расширения.

export interface MapPlugin {
  name: string;
  install(context: MapContext): void;
}

export class PluginManager {
  constructor(private context: MapContext) {}

  use(plugin: MapPlugin) {
    plugin.install(this.context);
  }
}

Такой подход позволяет вводить модульную архитектуру без нарушения целостности типов.


Композиция типов в масштабируемой архитектуре

При росте системы типы начинают комбинироваться:

  • объединение слоёв через union types;
  • композиция источников;
  • вывод зависимостей через generics;
  • построение связей между событиями и состоянием карты.
type MapState = {
  zoom: number;
  center: [number, number];
  activeLayerId?: string;
};

Типизированная обёртка превращает состояние карты в формально описанную структуру, пригодную для интеграции с внешними системами управления состоянием.


Контроль ошибок на уровне компиляции

Основной эффект введения типизированных обёрток проявляется в переносе класса ошибок из runtime в compile-time:

  • несоответствие структуры слоя;
  • неправильные идентификаторы источников;
  • некорректные типы событий;
  • нарушение контракта конфигурации карты.

Это снижает стоимость поддержки и повышает предсказуемость поведения системы при сложных сценариях визуализации геоданных.