Работа с TypeScript

TypeScript предоставляет строгую систему типов, которая значительно упрощает разработку сложных геопространственных приложений на базе Deck.gl. Благодаря типизации повышается надежность кода, улучшается автодополнение в редакторах, упрощается рефакторинг и снижается количество ошибок при работе с большими наборами данных и слоями визуализации.

Deck.gl включает встроенные TypeScript-определения, поэтому дополнительная установка пакетов типов обычно не требуется.

Настройка проекта

Установка основных зависимостей:

npm install deck.gl
npm install typescript

Минимальный файл конфигурации TypeScript:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Особенно рекомендуется включать режим строгой типизации:

{
  "strict": true
}

Этот режим помогает выявлять ошибки еще на этапе компиляции.


Типизация данных

Одним из важнейших аспектов работы с Deck.gl является описание структуры отображаемых объектов.

Допустим, имеется набор городов:

interface City {
  id: number;
  name: string;
  population: number;
  coordinates: [number, number];
}

Массив данных:

const cities: City[] = [
  {
    id: 1,
    name: "Almaty",
    population: 2000000,
    coordinates: [76.886, 43.238]
  },
  {
    id: 2,
    name: "Astana",
    population: 1300000,
    coordinates: [71.449, 51.169]
  }
];

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


Типизация слоев

Практически все слои Deck.gl поддерживают дженерики.

Пример создания ScatterplotLayer:

import {ScatterplotLayer} from '@deck.gl/layers';

interface City {
  id: number;
  name: string;
  population: number;
  coordinates: [number, number];
}

const layer = new ScatterplotLayer<City>({
  id: 'cities',

  data: cities,

  getPosition: city => city.coordinates,

  getRadius: city => city.population / 1000,

  getFillColor: () => [255, 140, 0]
});

После указания типа <City> все callback-функции автоматически получают корректный тип данных.

Например:

getPosition: city => city.coordinates

Переменная city будет иметь тип:

City

Поэтому редактор сможет подсказывать доступные свойства.


Использование LayerProps

Каждый слой обладает набором параметров, описанных через интерфейсы свойств.

Пример:

import {ScatterplotLayerProps} from '@deck.gl/layers';

Создание конфигурации слоя:

const props: ScatterplotLayerProps<City> = {
  id: 'cities',
  data: cities,
  getPosition: city => city.coordinates
};

Если будет передано недопустимое свойство:

const props: ScatterplotLayerProps<City> = {
  id: 'cities',
  invalidProperty: true
};

TypeScript немедленно сообщит об ошибке.


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

Deck.gl активно использует события взаимодействия.

Пример обработчика клика:

import {PickingInfo} from '@deck.gl/core';

const layer = new ScatterplotLayer<City>({
  id: 'cities',

  data: cities,

  pickable: true,

  onClick: (info: PickingInfo<City>) => {
    console.log(info.object?.name);
  }
});

Тип PickingInfo<T> позволяет получать строго типизированный объект.

Доступные свойства:

info.object
info.coordinate
info.index
info.layer
info.viewport
info.x
info.y

Тип объекта определяется автоматически:

City | null

Работа с nullable-значениями

При выборе объекта необходимо учитывать отсутствие выбранного элемента.

Небезопасный вариант:

console.log(info.object.name);

Безопасный вариант:

console.log(info.object?.name);

Либо:

if (info.object) {
  console.log(info.object.name);
}

Подобный подход особенно важен при включенном режиме strictNullChecks.


Типизация Deck

Экземпляр Deck также имеет собственные типы.

Создание визуализации:

import {Deck} from '@deck.gl/core';

const deck = new Deck({
  initialViewState: {
    longitude: 76.886,
    latitude: 43.238,
    zoom: 10
  },

  controller: true,

  layers: [layer]
});

Тип объекта:

Deck

После этого становятся доступны методы:

deck.setProps(...)
deck.pickObject(...)
deck.pickMultipleObjects(...)
deck.finalize()

Типизация ViewState

Состояние камеры желательно описывать отдельным интерфейсом.

Пример:

import {MapViewState} from '@deck.gl/core';

const viewState: MapViewState = {
  longitude: 76.886,
  latitude: 43.238,
  zoom: 11,
  pitch: 30,
  bearing: 0
};

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

Например:

const state: MapViewState = {
  longitude: 76.886,
  latitude: 43.238,
  zomm: 10
};

Компилятор сообщит об опечатке в свойстве zoom.


Типизация GeoJSON

Deck.gl часто используется совместно с GeoJSON.

Описание объекта:

interface CountryProperties {
  name: string;
  population: number;
}

Использование библиотеки GeoJSON:

import {Feature} from 'geojson';

type CountryFeature = Feature<
  GeoJSON.Geometry,
  CountryProperties
>;

Создание слоя:

import {GeoJsonLayer} from '@deck.gl/layers';

const layer = new GeoJsonLayer<CountryFeature>({
  id: 'countries',

  data: geojson,

  getFillColor: feature => {
    return feature.properties.population > 10000000
      ? [255, 0, 0]
      : [0, 255, 0];
  }
});

Теперь свойства GeoJSON доступны с полной типовой безопасностью.


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

Нередко данные содержат большое количество полей.

Пример:

interface Vehicle {
  id: string;
  model: string;
  speed: number;
  heading: number;
  coordinates: [number, number];
  online: boolean;
}

Использование:

new ScatterplotLayer<Vehicle>({
  id: 'vehicles',

  data: vehicles,

  getPosition: vehicle => vehicle.coordinates,

  getRadius: vehicle => vehicle.speed
});

Каждый аксессор автоматически получает тип Vehicle.


Типизация доступа к данным

В крупных проектах рекомендуется выносить вычисления в отдельные функции.

Например:

function getVehiclePosition(
  vehicle: Vehicle
): [number, number] {
  return vehicle.coordinates;
}

Использование:

new ScatterplotLayer<Vehicle>({
  getPosition: getVehiclePosition
});

Подход облегчает повторное использование логики и улучшает читаемость.


Наследование интерфейсов

TypeScript позволяет строить сложные иерархии данных.

Базовый интерфейс:

interface MapObject {
  id: string;
  coordinates: [number, number];
}

Наследник:

interface Vehicle extends MapObject {
  speed: number;
  heading: number;
}

Еще один наследник:

interface Sensor extends MapObject {
  temperature: number;
}

Теперь общие свойства не дублируются.


Использование типов объединения

Некоторые наборы данных содержат объекты разных типов.

Пример:

interface Vehicle {
  type: 'vehicle';
  speed: number;
}

interface Sensor {
  type: 'sensor';
  temperature: number;
}

type MapEntity = Vehicle | Sensor;

Проверка типа:

function process(entity: MapEntity) {
  if (entity.type === 'vehicle') {
    console.log(entity.speed);
  } else {
    console.log(entity.temperature);
  }
}

Такой механизм называется дискриминирующим объединением.


Типизация кастомных слоев

Deck.gl позволяет создавать собственные классы слоев.

Описание данных:

interface PointData {
  id: string;
  position: [number, number];
}

Создание класса:

import {Layer} from '@deck.gl/core';

export class CustomLayer extends Layer<PointData> {
}

Теперь весь слой работает с объектами типа PointData.


Типизация состояния слоя

Кастомный слой может хранить внутреннее состояние.

Описание:

interface LayerState {
  hoveredIndex: number;
}

Использование:

export class CustomLayer extends Layer<
  PointData,
  LayerState
> {
}

Доступ к состоянию:

this.state.hoveredIndex

Тип будет определен автоматически.


Использование Utility Types

TypeScript предоставляет встроенные служебные типы.

Partial

Делает свойства необязательными:

type PartialCity = Partial<City>;

Результат:

{
  id?: number;
  name?: string;
  population?: number;
  coordinates?: [number, number];
}

Pick

Выбор нескольких полей:

type CitySummary = Pick<
  City,
  'id' | 'name'
>;

Полученный тип:

{
  id: number;
  name: string;
}

Omit

Исключение полей:

type CityWithoutPopulation =
  Omit<City, 'population'>;

Record

Создание словаря:

const cityMap: Record<number, City> = {
  1: cities[0],
  2: cities[1]
};

Типизация асинхронной загрузки данных

Получение данных с сервера:

async function loadCities(): Promise<City[]> {
  const response = await fetch('/api/cities');

  return response.json();
}

Использование:

const cities = await loadCities();

Тип результата:

City[]

Generic-функции для работы со слоями

Создание универсального генератора слоев:

function createLayer<T>(
  id: string,
  data: T[],
  getPosition: (item: T) => [number, number]
) {
  return new ScatterplotLayer<T>({
    id,
    data,
    getPosition
  });
}

Использование:

const layer = createLayer(
  'cities',
  cities,
  city => city.coordinates
);

Тип City будет выведен автоматически.


Типизация React-компонентов с Deck.gl

Пример функционального компонента:

import DeckGL from '@deck.gl/react';

interface Props {
  cities: City[];
}

Компонент:

export function Map({cities}: Props) {
  const layer = new ScatterplotLayer<City>({
    id: 'cities',
    data: cities,
    getPosition: city => city.coordinates
  });

  return (
    <DeckGL
      controller={true}
      layers={[layer]}
    />
  );
}

Все свойства компонента и слоя остаются строго типизированными.


Практики организации типов

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

src/
 ├─ types/
 │   ├─ city.ts
 │   ├─ vehicle.ts
 │   ├─ sensor.ts
 │   └─ geojson.ts
 ├─ layers/
 ├─ services/
 └─ components/

Пример файла типов:

export interface City {
  id: number;
  name: string;
  population: number;
  coordinates: [number, number];
}

Импорт:

import {City} from '../types/city';

Централизация типов значительно упрощает сопровождение больших приложений.


Наиболее распространённые ошибки типизации

Отсутствие дженерика слоя

Нежелательный вариант:

new ScatterplotLayer({
  data: cities
});

Правильный вариант:

new ScatterplotLayer<City>({
  data: cities
});

Использование any

Нежелательно:

function process(data: any) {
}

Предпочтительно:

function process(data: City) {
}

Игнорирование nullable-значений

Ошибка:

console.log(info.object.name);

Безопасный вариант:

console.log(info.object?.name);

Отключение строгого режима

Нежелательно:

{
  "strict": false
}

Предпочтительно:

{
  "strict": true
}

Строгая типизация особенно полезна в проектах с большим количеством слоев, сложными структурами данных, взаимодействием с GeoJSON, обработкой пользовательских событий и разработкой собственных расширений Deck.gl. Грамотное использование интерфейсов, дженериков, объединений типов и встроенных средств TypeScript делает код визуализации более надежным, масштабируемым и удобным для сопровождения.