Общие паттерны типизации

Типизация в экосистеме MapLibre GL JS опирается на сочетание декларативных интерфейсов, структурных типов и расширяемых контрактов, которые отражают динамическую природу WebGL-картографического рендеринга и одновременно позволяют сохранить строгую проверку на этапе компиляции TypeScript. Основная сложность заключается в том, что модель данных карты включает сразу несколько уровней абстракции: геометрии GeoJSON, стили слоя, источники данных, события взаимодействия и расширяемые плагины.

В основе типизации MapLibre GL JS лежит структурный подход, при котором совместимость определяется не именем типа, а его формой. Это особенно важно для объектов конфигурации карты, где большинство сущностей описывается через набор опциональных и обязательных полей.

import maplibregl from "maplibre-gl";

const map = new maplibregl.Map({
  container: "map",
  style: "https://demotiles.maplibre.org/style.json",
  center: [0, 0],
  zoom: 2
});

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

Partial-конфигурации и гибкие интерфейсы

Конфигурационные объекты MapLibre активно используют Partial<T>, что позволяет задавать только необходимые поля:

type MapOptions = Partial<{
  container: string | HTMLElement;
  style: string | object;
  center: [number, number];
  zoom: number;
}>;

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

Типизация GeoJSON и работа с геометриями

Одним из ключевых элементов типизации выступает стандарт GeoJSON, который строго формализован через набор интерфейсов:

import type { FeatureCollection, Geometry } from "geojson";

const data: FeatureCollection<Geometry> = {
  type: "FeatureCollection",
  features: []
};

Здесь используется параметризация по типу геометрии, что позволяет ограничивать допустимые формы данных. Например, можно зафиксировать только точки:

import type { FeatureCollection, Point } from "geojson";

type PointCollection = FeatureCollection<Point>;

Такой подход критически важен при работе с источниками geojson в MapLibre, где несоответствие геометрии приводит к runtime-ошибкам рендеринга.

Типы источников данных (Sources)

MapLibre разделяет данные на источники, каждый из которых имеет собственную структуру. Основные типы включают vector, raster, geojson, image, video.

type Source =
  | GeoJSONSource
  | VectorSource
  | RasterSource
  | ImageSource
  | VideoSource;

Использование дискриминированных объединений позволяет безопасно работать с источниками:

function isGeoJSONSource(source: Source): source is GeoJSONSource {
  return source.type === "geojson";
}

Это обеспечивает корректную работу с API, где методы зависят от конкретного типа источника.

Типизация слоёв и стильовой спецификации

Стиль карты представляет собой иерархию слоёв, каждый из которых строго типизирован:

type Layer =
  | FillLayer
  | LineLayer
  | SymbolLayer
  | CircleLayer
  | HeatmapLayer
  | FillExtrusionLayer;

Каждый слой имеет обязательное поле type, выступающее дискриминатором:

interface FillLayer {
  id: string;
  type: "fill";
  source: string;
  paint?: FillPaint;
  layout?: FillLayout;
}

Типизация paint и layout реализуется через вложенные интерфейсы, отражающие свойства визуализации. Это позволяет на этапе компиляции исключать некорректные комбинации свойств.

Дискриминированные объединения в слоях

Ключевым паттерном является использование discriminated unions для слоёв:

type AnyLayer = {
  id: string;
  type: string;
} & (FillLayer | LineLayer | SymbolLayer);

Такой подход позволяет TypeScript автоматически сужать тип при проверке поля type:

function handleLayer(layer: AnyLayer) {
  if (layer.type === "line") {
    layer.paint.lineColor;
  }
}

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

MapLibre активно использует событийную модель, где события параметризуются типами данных:

map.on("click", (e) => {
  const lngLat = e.lngLat;
});

В TypeScript это выражается через перегрузки:

type MapMouseEvent = {
  type: string;
  lngLat: maplibregl.LngLat;
  point: maplibregl.Point;
};

Для повышения строгости используется сопоставление событий с конкретными типами:

type MapEventType =
  | "click"
  | "mousemove"
  | "load"
  | "idle";

Расширение типов через module augmentation

Одним из наиболее мощных механизмов является расширение типов через декларативное дополнение модулей:

declare module "maplibre-gl" {
  interface Map {
    customMethod(): void;
  }
}

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

Типизация пользовательских источников и слоёв

При создании кастомных слоёв используется строгий контракт:

interface CustomLayer {
  id: string;
  type: "custom";
  renderingMode: "2d" | "3d";
  onAdd(map: maplibregl.Map): void;
  render(gl: WebGLRenderingContext): void;
}

Такой интерфейс обеспечивает согласованность с жизненным циклом WebGL-рендеринга.

Генерики в API картографических структур

Генерики применяются для параметризации данных:

interface TypedSource<T> {
  data: T;
}

Это особенно полезно при работе с GeoJSON:

type TypedGeoJSONSource<T> = TypedSource<FeatureCollection<T>>;

Utility types и трансформация конфигураций

Внутренние конфигурации часто используют utility types:

  • Partial<T>
  • Required<T>
  • Pick<T, K>
  • Omit<T, K>

Пример:

type MinimalLayer = Pick<FillLayer, "id" | "type" | "source">;

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

Типизация выражений фильтрации

MapLibre поддерживает фильтры в стиле выражений, которые также типизируются:

type Filter =
  | ["==", string, any]
  | ["!=", string, any]
  | [">", string, number];

Это позволяет ограничить синтаксис выражений на уровне компиляции:

const filter: Filter = ["==", "type", "park"];

Безопасная работа с LngLat и Point

Географические координаты строго типизированы:

interface LngLat {
  lng: number;
  lat: number;
}

А экранные координаты отделены в отдельный тип:

interface Point {
  x: number;
  y: number;
}

Разделение этих пространств предотвращает логические ошибки при трансформации координат между слоями представления.

Типизация анимаций и easing-функций

Интерфейсы анимации используют функции высшего порядка:

type EasingFunction = (t: number) => number;

Это позволяет типизировать кастомные интерполяции:

const easeIn: EasingFunction = (t) => t * t;

Паттерны строгой изоляции API

Типизация MapLibre часто строится на принципе изоляции внутренних и внешних API. Внутренние структуры не экспортируются напрямую, а внешние интерфейсы предоставляются через стабильные типы.

Это достигается комбинацией:

  • export type
  • interface segregation
  • readonly полей
  • ограниченных union-типов

Иммутабельность конфигураций

Некоторые части API проектируются как неизменяемые:

type ReadonlyStyle = Readonly<StyleSpecification>;

Это снижает риск случайного изменения состояния карты после загрузки стиля.

Композиция типов слоёв и источников

Финальная модель типизации строится как композиция:

type MapEntity =
  | Source
  | Layer
  | StyleSpecification;

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