feat: derive buildOptions return type from PROPS table via Omit<PropTypesFromTable<TProps>, TExclude[number]>

Use a const type parameter on the exclude array so literal tuples narrow
correctly. The return type now reflects the actual properties and their
kinds (number/string/boolean) instead of Record<string, unknown>.

Four call sites needed local casts where PROPS declares str but Leaflet
expects a narrower type (crossOrigin, offset) — these are design-level
mismatches between HTML attribute types and Leaflet's option types.
main
Buddy 3 months ago
parent 348e865dae
commit 18db84e9a4

@ -1,4 +1,5 @@
import { ImageOverlay, LatLngBounds, LatLngExpression } from 'leaflet'; import { ImageOverlay, LatLngBounds, LatLngExpression } from 'leaflet';
import type { ImageOverlayOptions } from 'leaflet';
import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.js'; import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.js';
import { withProps } from '../core/with-props.js'; import { withProps } from '../core/with-props.js';
@ -27,7 +28,7 @@ export class LeafletImageOverlay extends withProps(HTMLElement, PROPS) {
this.#obj = new ImageOverlay( this.#obj = new ImageOverlay(
url, url,
parseBoundsAttr(this), parseBoundsAttr(this),
buildOptions(this, PROPS, ['url', 'bounds']), buildOptions(this, PROPS, ['url', 'bounds']) as ImageOverlayOptions,
); );
registerChildren(this, this.#obj); registerChildren(this, this.#obj);
} }

@ -1,4 +1,5 @@
import { SVGOverlay, LatLngBounds, LatLngExpression } from 'leaflet'; import { SVGOverlay, LatLngBounds, LatLngExpression } from 'leaflet';
import type { ImageOverlayOptions } from 'leaflet';
import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.js'; import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.js';
import { withProps } from '../core/with-props.js'; import { withProps } from '../core/with-props.js';
@ -25,7 +26,7 @@ export class LeafletSVGOverlay extends withProps(HTMLElement, PROPS) {
this.#obj = new SVGOverlay( this.#obj = new SVGOverlay(
svg ?? dummy!, svg ?? dummy!,
parseBoundsAttr(this), parseBoundsAttr(this),
buildOptions(this, PROPS, ['bounds']), buildOptions(this, PROPS, ['bounds']) as ImageOverlayOptions,
); );
registerChildren(this, this.#obj); registerChildren(this, this.#obj);
} }

@ -24,7 +24,7 @@ export class LeafletTileLayerWMS extends withProps(HTMLElement, PROPS) {
const url = this.getAttribute('url') || ''; const url = this.getAttribute('url') || '';
this.#obj = new TileLayer.WMS( this.#obj = new TileLayer.WMS(
url, url,
buildOptions(this, PROPS, ['url']) as Record<string, string>, buildOptions(this, PROPS, ['url']),
); );
registerChildren(this, this.#obj); registerChildren(this, this.#obj);
} }

@ -1,4 +1,5 @@
import { Tooltip } from 'leaflet'; import { Tooltip } from 'leaflet';
import type { TooltipOptions } from 'leaflet';
import { registerWithParent, buildOptions, numAttr } from '../core/utils.js'; import { registerWithParent, buildOptions, numAttr } from '../core/utils.js';
import { withProps } from '../core/with-props.js'; import { withProps } from '../core/with-props.js';
@ -23,7 +24,7 @@ export class LeafletTooltip extends withProps(HTMLElement, PROPS) {
this.#obj = new Tooltip({ this.#obj = new Tooltip({
...buildOptions(this, PROPS, ['lat', 'lng']), ...buildOptions(this, PROPS, ['lat', 'lng']),
content: this.innerHTML, content: this.innerHTML,
}); } as unknown as TooltipOptions);
if (this.hasAttribute('lat') && this.hasAttribute('lng')) { if (this.hasAttribute('lat') && this.hasAttribute('lng')) {
this.#obj.setLatLng([numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')]); this.#obj.setLatLng([numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')]);
} }

@ -1,4 +1,5 @@
import { VideoOverlay, LatLngBounds, LatLngExpression } from 'leaflet'; import { VideoOverlay, LatLngBounds, LatLngExpression } from 'leaflet';
import type { VideoOverlayOptions } from 'leaflet';
import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.js'; import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.js';
import { withProps } from '../core/with-props.js'; import { withProps } from '../core/with-props.js';
@ -28,7 +29,7 @@ export class LeafletVideoOverlay extends withProps(HTMLElement, PROPS) {
this.#obj = new VideoOverlay( this.#obj = new VideoOverlay(
url, url,
parseBoundsAttr(this), parseBoundsAttr(this),
buildOptions(this, PROPS, ['url', 'bounds']), buildOptions(this, PROPS, ['url', 'bounds']) as VideoOverlayOptions,
); );
registerChildren(this, this.#obj); registerChildren(this, this.#obj);
} }

@ -1,4 +1,4 @@
import type { PropDef } from '../types/props.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js';
import type { LatLngBoundsExpression } from 'leaflet'; import type { LatLngBoundsExpression } from 'leaflet';
// Converts kebab-case to camelCase (e.g. "fill-color" → "fillColor"). // Converts kebab-case to camelCase (e.g. "fill-color" → "fillColor").
@ -108,12 +108,20 @@ export function setLayerAttr(
// constructors, like coordinates or URLs). Null attributes use the // constructors, like coordinates or URLs). Null attributes use the
// default from PROPS if one exists (skipping empty-string defaults to // default from PROPS if one exists (skipping empty-string defaults to
// avoid Leaflet rejecting them). // avoid Leaflet rejecting them).
export function buildOptions( //
// The return type is derived from the PROPS table: each prop name maps
// to its kind's value type (number for `num`, boolean for `bool-on`/`bool-off`,
// string for `str`). The `const` type parameter makes literal exclude arrays
// narrow correctly so excluded keys are stripped from the return type.
export function buildOptions<
TProps extends Record<string, PropDef>,
const TExclude extends readonly string[] = [],
>(
el: HTMLElement, el: HTMLElement,
props: Record<string, PropDef>, props: TProps,
exclude: string[] = [], exclude: TExclude = [] as unknown as TExclude,
): Record<string, unknown> { ): Omit<PropTypesFromTable<TProps>, TExclude[number]> {
const opts: Record<string, unknown> = {}; const opts = {} as Record<string, unknown>;
for (const [propName, spec] of Object.entries(props)) { for (const [propName, spec] of Object.entries(props)) {
if (exclude.includes(propName)) continue; if (exclude.includes(propName)) continue;
const val = el.getAttribute(spec.attr); const val = el.getAttribute(spec.attr);
@ -126,7 +134,7 @@ export function buildOptions(
else if (spec.kind === 'bool-off') opts[propName] = false; else if (spec.kind === 'bool-off') opts[propName] = false;
else opts[propName] = val; else opts[propName] = val;
} }
return opts; return opts as Omit<PropTypesFromTable<TProps>, TExclude[number]>;
} }
// Dispatches a custom `leaflet-register` event upward through the DOM // Dispatches a custom `leaflet-register` event upward through the DOM

Loading…
Cancel
Save