mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.25k stars 2.23k forks source link

Migrating from `@types/mapbox-gl` to first-class TypeScript typings #13203

Open stepankuzmin opened 5 months ago

stepankuzmin commented 5 months ago

Migrating from @types/mapbox-gl to first-class TypeScript typings

The GL JS v3.5.0 release marks a significant transition for GL JS, moving from Flow to TypeScript. While we have maintained backward compatibility where possible, the community typings @types/mapbox-gl are not fully compatible with the new first-class typings. Users relying on the community typings may experience breaking changes. This guide will help you migrate to the new first-class typings and resolve common issues.

Please feel free to comment on this issue if you encounter difficulties during migration. Share your experiences and suggestions to support one another.

Updating GL JS

Install the latest version of GL JS and remove the @types/mapbox-gl dependency.

npm install mapbox-gl@3.5.0
npm uninstall @types/mapbox-gl

Run the TypeScript compiler (tsc) to check for the errors.

Common issues

The migration should be straightforward since there's no need to change how you interact with the API; only the types have changed. However, you may encounter some common issues.

Dangling @types/mapbox-gl

Ensure you're using the latest version of GL JS and have removed the @types/mapbox-gl dependency.

Deprecated Features

Community typings provided features deprecated since v1 and v2, such as the optimizeForTerrain map option, tiledata and tiledataloading events, zoom and property functions, certain exports like the mapboxgl.Control. Please refer to the compatibility test suite in test/build/typings/compatibility-test.ts, used to test first-class typings compatibility with the community typings. Tests incompatible with mapbox-gl typings are marked with @ts-expect-error - incompatible.

Naming Conventions

Community typings' naming convention slightly differs from those provided with GL JS:

Note: We've created aliases where possible (e.g., MapboxOptions as an alias to MapOptions) and marked these aliases as @deprecated in the first-class typings (this might be visible in your editor, e.g., with strikethrough style in VSCode IntelliSense). However, due to potential collisions, we couldn't create aliases for all cases. For example, we couldn't alias Source to SourceSpecification because GL JS already exports Source (same as AnySourceImpl in community typings). We recommend using the new naming convention, but aliases will help you migrate smoothly. Please note that these deprecated types will be removed in future releases.

slavanga commented 5 months ago

Some learnings from my migration:

In the code below I'm getting a TS error for the base key. Any pointers how to fix or refactor that?

Object literal may only specify known properties, and 'base' does not exist in type 'ExpressionSpecification | { type: "exponential"; stops: [number, number][]; } | { type: "interval"; stops: [number, number][]; } | { type: "exponential"; stops: [...][]; property: string; default?: number | undefined; } | ... 5 more ... | { ...; }'.ts(2353)
mapRef.current?.addLayer({
  id: 'unclustered-point',
  type: 'circle',
  source: SOURCE_ID,
  filter: ['!', ['has', 'point_count']],
  paint: {
    'circle-radius': {
      base: 1.5, // <-- TS error here
      stops: [
        [4, 8],
        [10, 16],
      ],
    },
    'circle-color': circleColor,
  },
});
stepankuzmin commented 5 months ago

Hi @slavanga,

Thanks for sharing your learnings! The error in your case is caused by using the deprecated zoom function syntax. While the older syntax is still supported, the type we use for data-driven expressions doesn’t include it. You can refer to this comparison as an example of migrating to the newer syntax:

FunctionExpression
// make circles larger as the user
// zooms from z12 to z22
'circle-radius': {
    'base': 1.75,
    'stops': [[12, 2], [22, 180]]
},
// make circles larger as the user 
// zooms from z12 to z22
'circle-radius': [
    "interpolate",
    ["exponential", 1.75],
    ["zoom"],
    12, 2,
    22, 180
],

In your case, it should be:

'circle-radius': [
    'interpolate',
    ['exponential', 1.5],
    ['zoom'],
    4, 8,
    10, 16
]

I understand that it may not be convenient, so in the stable release, we could extend the type we provide to include the older syntax, making the migration process smoother for everyone.

slavanga commented 5 months ago

@stepankuzmin Got it! Thank you for the detailed explanation. I actually think the new syntax is more clear.

driera commented 4 months ago

Hi, I'm starting the migration to v3.5 and so far all deprecated types are easy to update, except one, which I'm having problems with:

MapboxGeoJSONFeature has been deprecated in favor of GeoJSONFeature, but typescript complains that it's not being exported. Any idea?

happy-turtle commented 4 months ago

I started the migration today and it went mostly well, but I got one small issue: At one point we use CustomLayerInterface, but this interface is not exported. As the specification for Map.addLayer looks like this Map$1.addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string), I would assume the CustomLayerInterface should be exported?

Similarly EasingOptions is not exported for e.g. flyTo().

vbraun commented 4 months ago

Some more feedback

1) MapboxGeoJSONFeature has been deprecated in favor of GeoJSONFeature, but the latter doesn't have the source field. In the typings it is called QueryFeature but that is not exported. So if you need to name it and access the source field you have to do something fugly like

    type QueryFeature = ReturnType<Map['queryRenderedFeatures']>[0];

2) GeolocateControl.on does not support the 'trackuserlocationstart' and 'trackuserlocationend' events, fails with

    Argument of type '"trackuserlocationstart"' is not assignable to parameter of type 'MapEvent'
Really `GeolocateControl.on` should extend `Evented.on` with the additional event types.

3) In general the map event typings are worse than the community-provided typings. You can't express that the click event has a point field, and the idle event does not. Ok you get a star for actually having first-party typings, but pretty please have a good look at the community-provided ones for improvements.

airbreather commented 4 months ago

For Angular applications that use the ever-stagnating ngx-mapbox-gl package which provides Angular wrappers around the Mapbox types, you must now set skipLibCheck to true in your TypeScript config, in addition to the legacy peer deps workaround that you have had to do for a while now.

chunlampang commented 4 months ago

Some types are not exported, such as Marker Options, ControlPosition, EasingOptions, StyleImageInterface

chunlampang commented 4 months ago
const scale = new mapboxgl.ScaleControl({ maxWidth: 80 });
map.addControl(scale, controlsPosition);

Argument of type 'ScaleControl' is not assignable to parameter of type 'IControl'. Types of property '_setLanguage' are incompatible.

stepankuzmin commented 4 months ago

Thanks for the feedback. We appreciate your patience while we enhance the developer experience. We are actively working on improving TypeScript support, and the upcoming patch release will address some of the issues highlighted here. Stay tuned for updates.

samuelcole commented 4 months ago

three things (and some of this could be ReactMapGL feedback):

map.on("click", (e: mapboxgl.MapMouseEvent) => {

seems like typescript should be able to tell which event string goes to which event type, so having that set up would be nice

onLoad={(e) => setMap(e.target as Map)}

a little annoying that the target is unknown

map.addLayer({
  id: "containedZipCodes",
  type: "fill",
  source: {
  ~~~~~~
    type: "vector",
    url: "mapbox://mapbox.boundaries-pos4-v4",
  },
  slot: "bottom",
  "source-layer": "boundaries_postal_4",
  paint: getPaint(allZipCodes),
  });

Type '{ type: string; url: string; }' is not assignable to type 'string'

when i change it to the string "mapbox://mapbox.boundaries-pos4-v4" it does not work, while this object does, so i suspect the type is wrong

i'll add // @ts-expect-error -- source needs to define a vector object while i wait for a fix for that

chunlampang commented 4 months ago
new mapboxgl.Popup().on('close', () => {});

Argument of type '"close"' is not assignable to parameter of type 'MapEvent'.

jeremy-wl-app-lite commented 4 months ago

I get this error in node_modules\mapbox-gl\dist\mapbox-gl.d.ts

Property 'tileID' in type 'ImageSource' is not assignable to the same property in base type 'ISource'. Type 'CanonicalTileID | null | undefined' is not assignable to type 'CanonicalTileID | undefined'. Type 'null' is not assignable to type 'CanonicalTileID | undefined'.

ImageSource.titleId: CanonicalTileID | null | undefined; ISource.tileID?: CanonicalTileID;

olivvybee commented 4 months ago

The types in 3.5.1 are disallowing passing a number as the padding option in map.fitBounds():

map.fitBounds(bounds, {
    maxZoom: 12,
    padding: 80,
//  ~~~~~~~
//  Type 'number' is not assignable to type 'PaddingOptions'. ts(2322)
//  (property) padding?: PaddingOptions | undefined
});

According to the API reference this should still be supported (and my brief skim of the code confirms this)

chkl commented 4 months ago

I encountered a few types that are used for arguments on public methods but are not exported:

stepankuzmin commented 4 months ago

Hi everyone. We've just published the GL JS v3.5.2 patch release, which includes TypeScript API improvements such as strongly typed Map event listeners, improved type narrowing, and explicit exports for some previously missed types. Please try out the new version and let us know how it works for you.

Thanks again for the feedback. I'll keep this issue open for any new findings.

orosmatthew commented 4 months ago

I believe type CircleLayerSpecification.filter should be type FilterSpecification | ExpressionSpecification instead of just FilterSpecification

ezzatron commented 4 months ago

@stepankuzmin A couple of remaining type issues in 3.5.2:

Screenshot 2024-07-19 at 09 12 21 Screenshot 2024-07-19 at 09 12 45
chunlampang commented 3 months ago
map.addLayer({
  'id': 'tower',
  'type': 'model',
  'source': 'model',
  'layout': {
    'model-id': ['get', 'model-uri']
  },
  'paint': {
    'model-cast-shadows': true,
  }
});

Type 'boolean' is not assignable to type '[string, ...any[]]'.ts(2322) (property) "model-cast-shadows"?: ExpressionSpecification | undefined

chunlampang commented 3 months ago
map.addLayer({
  'id': 'tower',
  'type': 'model',
  'source': 'model',
  'layout': {
    'model-id': ['get', 'model-uri']
  },
  'paint': {
    'model-rotation':  [0.0, 0.0, ['get', 'rotate']],
  }
});

Type '[number, number, string[]]' is not assignable to type 'DataDrivenPropertyValueSpecification<[number, number, number]> | undefined'. Type '[number, number, string[]]' is not assignable to type 'ExpressionSpecification | [number, number, number]'.ts(2322) (property) "model-rotation"?: DataDrivenPropertyValueSpecification<[number, number, number]> | undefined

chunlampang commented 3 months ago
map.setLights([
  {
    "id": "directional",
    "type": "directional",
    "properties": {
      "cast-shadows": true,
    }
  }
])

Type 'boolean' is not assignable to type '[string, ...any[]]'.ts(2322) (property) "cast-shadows"?: ExpressionSpecification | undefined

Wouter125 commented 3 months ago

Spotted a small little mistake when updating from 3.4.0 to 3.6.0 and using the mapbox-gl types. TransformRequestFunction seems to be deprecated and replaced by RequestTransformFunction. But RequestTransformFunction is not exposed inside the package causing type issues. Any chance RequestTransformFunction can get exposed on 3.6.1?

Apart from that seems like the Map type references to itself or something because as soon as I try to pass it as an argument through one of my functions; Type instantiation is excessively deep and possibly infinite.ts(2589). It's not a major issue and understandable from the Map perspective, but maybe it can be addressed.

MrDiablon commented 3 months ago

Hello,

It's look like a mistake is made for the function addLayer

My code:

map?.addLayer(
      {
        id,
        layout: {
          'line-join': 'round',
          'line-cap': 'round',
        },
        paint: {
          'line-color': ['get', 'color'],
          'line-width': 6,
        },
        source: {
          type: 'geojson',
          data: {
            type: 'FeatureCollection',
            features: features(),
          },
        } as unknown as mapboxgl.GeoJSONSource,
        type: 'line',
      },
      'lines',
    )
  }

I got the next error:

image

I check the documentation and we can use an object and it's required when the type isn't custom or background like in my case

darkbasic commented 2 months ago

Apart from that seems like the Map type references to itself or something because as soon as I try to pass it as an argument through one of my functions; Type instantiation is excessively deep and possibly infinite.ts(2589). It's not a major issue and understandable from the Map perspective, but maybe it can be addressed.

This is pretty annoying, any chance to fix it?

darkbasic commented 2 months ago

I also don't quite understand why new mapboxgl.Map() returns Map$1 instead of mapboxgl.Map: image