[Bug] `Layer.getBounds()` type is not compatible with `WebMercatorViewport.fitBounds()`
lsh opened this issue · comments
Description
If one looks at the the fitBounds docs, the example usage is the following:
const [initialViewState, setInitialViewState] = useState<MapViewState>({
longitude: -100,
latitude: 40,
zoom: 4
});
const [hasLoaded, setHasLoaded] = useState<boolean>(false);
const layer = new ScatterplotLayer({...});
const onAfterRender = () => {
if (!hasLoaded && layer.isLoaded) {
setHasLoaded(true);
const viewport = layer.context.viewport as WebMercatorViewport;
const {longitude, latitude, zoom} = viewport.fitBounds(layer.getBounds());
setInitialViewState({longitude, latitude, zoom});
}
};
However, the return type of ScatterPlotLayer.getBounds()
will be [number[], number[]] | null
, while WebMercatorViewport.fitBounds
takes in a [[number, number], [number, number]]
. The example code fails to type check as a result of this issue both because layer.getBounds()
could return null
and because number[]
cannot be coerced to [number, number]
. The first issue is easy to fix with a null check, but the second requires either an as
cast or some more arduous checks.
Flavors
- Script tag
- React
- Python/Jupyter notebook
- MapboxOverlay
- GoogleMapsOverlay
- CartoLayer
- ArcGIS
Expected Behavior
If the output of Layer.getBounds()
is truthy, its type should be accepted in WebMercatorView.fitBounds()
.
Steps to Reproduce
Follow the instructions on the fitBounds()
docs to set up a Deck.GL scene.
Environment
- Framework version: 8.9.6
- Browser: Chrome
- OS: MacOs
Logs
No response
@chrisgervang @zbigg Do you have any suggestions how we can better handle this? Both types are technically correct:
layer.getBounds()
can be any dimensions depending on the attribute sizeviewport.fitBounds()
will lose proper type hint if we loosen it tonumber[][]
Hmm, maybe use a type guard function? Does something like this work?
function isBounds(bounds: any): bounds is [[number, number], [number, number]] {
return Array.isArray(bounds) &&
bounds.length === 2 &&
bounds.every(coord => Array.isArray(coord) && coord.length === 2;
}
const bounds = layer.getBounds();
if (bounds && isBounds(bounds)) {
const { longitude, latitude, zoom } = viewport.fitBounds(bounds);
setInitialViewState({ longitude, latitude, zoom });
}
Otherwise, I've only thought of solutions already mentioned. Type assertion or loosen the type.
Unfortunately i don't have any trick that can help us. We're left with assertion or loosened type.
Note, I've tried also to override type of ScatterPlotLayer.getBounds
to return [[number,number,number], [number,number, number]]
(actual type), but it's not assignable too:
Argument of type '[XyzPosition, XyzPosition]' is not assignable to parameter of type 'LatLonBounds'.
Type at position 0 in source is not compatible with type at position 0 in target.
Type 'XyzPosition' is not assignable to type 'LatLon'.
Source has 3 element(s) but target allows only 2.(2345)
Looks like TS is too strict for our case and i see no way to loosen those checks.
Playground
Pushed by curiosity, i've found one solution to strict checking of tuple arity this SO answer:
export type NonEmptyArray<Type> = [Type, ...Array<Type>];
That declares tuple type that matches 1 or elements but not 0.
Extended to 2 or more, we can create types like:
type Vec1D<Type> = [Type, ...Array<Type>];
type Vec2D<Type> = [Type, Type, ...Array<Type>];
type Vec3D<Type> = [Type, Type, Type, ...Array<Type>];
And if we use such types in getBounds
/ fitBounds
we can have proper type checking as long as ScatterPlotLayer type is not erased. Accessing getBounds through Layer interface will still return potentially 0 or 1 dimension bounds, so we're out of luck.
Having found this, i still think it's rather obscure technique and i wouldn't recommend complicating typings so much.
type LatLon = [number, number, ...number[]]
type Vec3D = [number, number, number, ...number[]];
class Layer {
getBounds(): [number[], number[]] {
return [[1, 2],[4, 5]];
}
}
class ScatterPlotLayer extends Layer {
getBounds(): [Vec3D, Vec3D] {
return super.getBounds() as [Vec3D, Vec3D];
}
}
function fitBoundsStrict(bounds: [LatLon, LatLon]) {
console.log(bounds);
}
const layer = new Layer()
const scatterPlotLayer = new ScatterPlotLayer()
fitBoundsStrict(scatterPlotLayer.getBounds()); // ok
fitBoundsStrict(layer.getBounds()); // error