ol-contextmenu includes full TypeScript support with comprehensive type definitions.
npm install ol ol-contextmenuType definitions are included automatically—no need for @types packages.
📖 For detailed installation instructions, see Getting Started.
import ContextMenu, {
type Item,
type Options,
type SingleItem,
type CallbackObject,
type ContextMenuEvent,
} from 'ol-contextmenu';
import type Map from 'ol/Map';
import type { Coordinate } from 'ol/coordinate';Note: Use type imports for better tree-shaking and to avoid runtime imports.
import ContextMenu, { type Item } from 'ol-contextmenu';
import type Map from 'ol/Map';
const items: Item[] = [
{
text: 'Center map here',
callback: (obj, map: Map) => {
map.getView().animate({
center: obj.coordinate,
duration: 700,
});
},
},
'-', // Separator (type-safe)
{
text: 'Zoom In',
callback: (obj, map: Map) => {
const view = map.getView();
view.animate({
zoom: view.getZoom()! + 1,
duration: 500,
});
},
},
];
const contextmenu = new ContextMenu({
width: 170,
defaultItems: true,
items,
});Union type representing any menu item:
type Item = SingleItem | ItemWithNested | ItemSeparator;A standard menu item with a callback:
interface SingleItem {
text: string;
callback: (obj: CallbackObject, map: Map) => void;
icon?: string;
classname?: string;
data?: any;
}A menu item containing nested subitems:
interface ItemWithNested {
text: string;
items: Item[];
icon?: string;
classname?: string;
}Menu separator (literal type):
type ItemSeparator = '-';Constructor options:
interface Options {
width?: number;
defaultItems?: boolean;
items?: Item[];
eventType?: EventTypes;
}Data passed to callbacks:
interface CallbackObject {
coordinate: Coordinate;
data?: any;
}Event data for beforeopen and open events:
class ContextMenuEvent extends BaseEvent {
map: Map;
coordinate: Coordinate;
pixel: Pixel;
originalEvent: MouseEvent;
}Enum for event trigger types:
enum EventTypes {
CONTEXTMENU = 'contextmenu',
CLICK = 'click',
DBLCLICK = 'dblclick',
}Enum for menu events:
enum CustomEventTypes {
BEFOREOPEN = 'beforeopen',
OPEN = 'open',
CLOSE = 'close',
}import type { CallbackObject } from 'ol-contextmenu';
import type Map from 'ol/Map';
const handleCenter = (obj: CallbackObject, map: Map): void => {
map.getView().setCenter(obj.coordinate);
};
const handleZoom = (obj: CallbackObject, map: Map): void => {
const view = map.getView();
const currentZoom = view.getZoom();
if (currentZoom !== undefined) {
view.setZoom(currentZoom + 1);
}
};
const items: Item[] = [
{ text: 'Center', callback: handleCenter },
{ text: 'Zoom In', callback: handleZoom },
];interface MarkerData {
id: number;
name: string;
type: 'marker' | 'poi';
}
const markerItem: SingleItem = {
text: 'Delete Marker',
data: {
id: 1,
name: 'My Marker',
type: 'marker',
} as MarkerData,
callback: (obj) => {
const data = obj.data as MarkerData;
console.log(`Deleting ${data.type}: ${data.name}`);
},
};import { CustomEventTypes, type ContextMenuEvent } from 'ol-contextmenu';
contextmenu.on(CustomEventTypes.BEFOREOPEN, (evt: ContextMenuEvent) => {
console.log('Clicked at:', evt.coordinate);
console.log('Pixel:', evt.pixel);
console.log('Map:', evt.map);
});
contextmenu.on(CustomEventTypes.OPEN, (evt: ContextMenuEvent) => {
const feature = evt.map.forEachFeatureAtPixel(
evt.pixel,
(ft) => ft,
);
if (feature) {
console.log('Clicked on feature:', feature.getId());
}
});const nestedItems: Item[] = [
{
text: 'Tools',
items: [
{
text: 'Measure',
items: [
{
text: 'Distance',
callback: measureDistance,
},
{
text: 'Area',
callback: measureArea,
},
],
},
'-',
{
text: 'Export',
callback: exportMap,
},
],
},
];import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import ContextMenu, {
type Item,
type CallbackObject,
CustomEventTypes,
} from 'ol-contextmenu';
// Initialize map
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM(),
}),
],
view: new View({
center: [0, 0],
zoom: 2,
}),
});
// Vector layer for markers
const vectorSource = new VectorSource();
const vectorLayer = new VectorLayer({
source: vectorSource,
});
map.addLayer(vectorLayer);
// Type-safe callback functions
const addMarker = (obj: CallbackObject, map: Map): void => {
const marker = new Feature({
geometry: new Point(obj.coordinate),
});
vectorSource.addFeature(marker);
console.log('Marker added at', obj.coordinate);
};
const centerMap = (obj: CallbackObject, map: Map): void => {
map.getView().animate({
center: obj.coordinate,
duration: 700,
});
};
// Define menu items
const items: Item[] = [
{
text: 'Add Marker',
icon: './marker.png',
callback: addMarker,
},
'-',
{
text: 'Center Here',
callback: centerMap,
},
];
// Create context menu
const contextmenu = new ContextMenu({
width: 180,
defaultItems: true,
items,
});
// Add to map
map.addControl(contextmenu);
// Event handlers
contextmenu.on(CustomEventTypes.BEFOREOPEN, (evt) => {
const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
if (feature) {
contextmenu.clear();
contextmenu.push({
text: 'Delete Marker',
callback: () => {
vectorSource.removeFeature(feature);
},
});
} else {
contextmenu.clear();
contextmenu.extend(items);
contextmenu.extend(contextmenu.getDefaultItems());
}
});Enable strict mode in tsconfig.json for maximum type safety:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitAny": true,
"noImplicitThis": true
}
}Solution: Make sure the package is installed:
npm install ol-contextmenuProblem:
// ❌ Error: Type '() => void' is not assignable...
const item = {
text: 'Test',
callback: () => console.log('test'),
};Solution:
// ✅ Correct: Include parameters
const item: SingleItem = {
text: 'Test',
callback: (obj, map) => console.log('test'),
};Problem:
// ❌ Wrong
import { ContextMenu, Item } from 'ol-contextmenu';Solution:
// ✅ Correct
import ContextMenu, { type Item } from 'ol-contextmenu';