位置エン本のサンプルを TypeScript で行うためのメモ
位置エン本のサンプルコードはすべて JavaScript で記述されている。
TypeScript でこれらのサンプルを写経する際、.js ファイルを .ts に置き換え、型情報を変数や関数の引数に追加するだけでは動かない部分があるため、この記事では動かない部分と対策をまとめた。
4章
サンプル全般
位置エン本のサンプルでは HTML 内に JavaScript を直接記述する形になっている。TypeScript を使う都合上、<script>
タグを使って TypeScript を変換した JavaScript ファイルを呼び出す形にする。
使用するライブラリは、MapLibre GL JS に関する部分はバージョンアップで TypeScript 対応が良くなっているため、できるだけ新しいバージョン(v4.7.0)を使用するように心がけた。
Leafletを使うサンプル
GeoJSON ファイルを扱うサンプル全般
L.GeoJSON
型や L.Marker
型はジェネリクスなので、GeoJSON のプロパティの型定義を事前に行い、その型定義をジェネリクスに代入すれば、適切な型定義が得られる。
type n02_21_geojson_prop = {
['N02_001']: string,
['N02_002']: '1'|'2'|'3'|'4'|'5',
['N02_003']: string,
['N02_004']: string,
}
L.geoJSON<n02_21_geojson_prop>(json, {
style: (feature) => {
// feature には undefined の可能性がある(後述)
const lineType = feature?.properties.N02_002;
if (lineType) {
// lineType の型は '1'|'2'|'3'|'4'|'5' になる
return {
weight: weightDict[lineType], // 事業者種別コードから線の太さを得る
color: colorDict[lineType], // 事業者種別コードから線の色を得る
}
}
return {};
},
})
bindPopup() 関数全般
- bindPopup() の第一引数に関数を代入する場合、代入する関数の第一引数が
@types/leaflet
ではL.Layer
になっているため、GeoJSON ファイルから文字列を得たい場合、L.Marker
などの型アサーションがないとエラーになる -
L.Content
の型の関係上、代入する関数の戻り値は文字列かHTMLElement
のどちらかにする必要がある
06_1_manyfigure
- レイヤー切り替えコントロールの
L.control.layers()
の第二引数は、JavaScript だと空配列で問題なかったが、TypeScript では空オブジェクトでないとエラーになる -
L.getJSON()
のstyle
オプションに関数を代入する場合、関数の型はStyleFunction<P>
になるが、この関数の第一引数の features は Optional になっているため、それを想定したコードにする必要がある(下記参照)
const polygon = L.geoJSON<a16_15_geojson_prop>(json, {
style: (feature) => {
// TypeScript 向け: undefined の可能性を取り除かないとエラーになる
if (feature) {
return {
color: 'red',
stroke: false,
// 人口を面積で割った値でポリゴンの濃さを変える
fillOpacity:
feature.properties['人口'] /
feature.properties['面積'] /
20000,
}
}
return {}
}
})
MapLibre GL JS を使うサンプル
サンプル全般
2024/10 時点では不要となった処理
- 公式の型定義は ESM を想定しているが、サンプルでは UMD 形式のファイルを使用しているため、下記のような型定義がないとグローバル変数で使用できない
- 5章では Vite で自動変換するため、この型定義は不要となる
declare global {
const maplibregl: typeof import('maplibre-gl')
}
export type {}
08_2_image_maplibre
位置エン本が発売されたときとは異なり、maplibre.Map()
のオプションに customAttribution
で直接設定することが出来なくなった。attributionControl
に変更して下記のように customAttribution
を設定する。
const map = new maplibregl.Map({
// 省略
zoom: 9,
attributionControl: {
customAttribution:
'<a href="https://maps.gsi.go.jp/development/ichiran.html">地理院タイル</a>',
},
// 省略
})
5章
MapLibre GL JS のバージョン
4章と同様に、MapLibre GL JS v4.7.0 を使用するように心がけた。
MapLibre GL Opacity のバージョン
- v1.7.0 から TypeScript の型定義が付属しているのでそれ以降のバージョンを使用する
MapGeoJSONFeature
型からの経度・緯度の取り出し
サンプルコードでは、何度か MapGeoJSONFeature
型から経度・緯度を取り出す処理がある。feature.geometory
が Geometory
型であるため、そのままだと型定義上では coordinate
プロパティが存在せず、エラーになる。Point
型ならば、coordinate
プロパティは存在するので型アサーションを行う。
const nearestFeature = features.reduce((minDistFeature: MapGeoJSONFeature, feature) => {
const dist = distance(
[longitude, latitude],
(<Point>feature.geometry).coordinates,
);
}
ただし、クリックイベントでポップアップをする処理ではこれだけだとエラーになる。
ポップアップ位置の経度・緯度を指定するが、この値は LngLatLike
型になっている必要がある。coordinates
プロパティの型は、Position
型であるがこれは LngLatLike
型に含まれない型なので LngLatLike
型に型アサーションを行う。
const feature = features[0];
const popup = new maplibregl.Popup()
.setLngLat((<Point>feature.geometry).coordinates as LngLatLike)
経緯度を渡すと最寄りの指定緊急避難場所を返す部分の実装
現在地に最も近い地物を見つける部分の実装
JavaScript の実装では features.reduce()
のコールバック関数の第一引数及び、第二引数の型が異なるため、TypeScript にそのまま持ってくると型の不一致でエラーになる。
コールバック関数について、TypeScript では以下の2点を満たすような実装を行う(下記参照)
-
features.reduce()
は第一引数のコールバック関数のみ使用する - コールバック関数で最小距離の初回更新時は
minDistFeature.properties
に dist プロパティが存在しないことを条件とする
// 現在地に最も近い地物を見つける
const nearestFeature = features.reduce((minDistFeature: MapGeoJSONFeature, feature) => {
const dist = distance(
[longitude, latitude],
(<Point>feature.geometry).coordinates,
);
if (!minDistFeature.properties.hasOwnProperty('dist') || minDistFeature.properties.dist > dist) {
const nextFeature = feature;
nextFeature.properties.dist = dist;
return nextFeature;
}
return minDistFeature;
});
現在表示中の指定緊急避難場所レイヤーを特定する実装
指定緊急避難場所レイヤーは "type": "circle"
で実装しているので、skhbから始まるlayerを抽出した後は、CircleLayerSpecification
型の配列が返ってくる。
/*
* TypeScript 向け: 指定緊急避難場所レイヤーは "type": "circle" なので
* CircleLayerSpecification 型にする
*/
const skhbLayers = style.layers.filter((layer) =>
// `skhb`から始まるlayerを抽出
layer.id.startsWith('skhb'),
) as Array<CircleLayerSpecification>;
const visibleSkhbLayers = skhbLayers.filter(
// 現在表示中のレイヤーを見つける
(layer) => layer.layout?.visibility === 'visible',
);
return visibleSkhbLayers[0].filter; // 表示中レイヤーのfilter条件を返す
現在地からの最寄りの指定緊急避難場所取得時の実装
2024/07 時点では不要となった処理
変数 currentSkhbLayerFilter
に getCurrentSkhbLayerFilter()
から値を代入した後の処理で、map.querySourceFeatures()
の filter 条件に currentSkhbLayerFilter
を使用するが、位置エン本が発売されてしばらくの間は currentSkhbLayerFilter
に含まれる、boolean
型の影響でエラーになっていた。そのため下記の実装が必要だった。
// TypeScript 向け: map.querySourceFeatures で使用する filter は
// any[] 型なので、boolean は除外する
if (Array.isArray(currentSkhbLayerFilter) === false) {
return;
}
// ここまでが必要だった
const features = map.querySourceFeatures('skhb', {
sourceLayer: 'skhb',
filter: currentSkhbLayerFilter,
});
2024/07 時点では不要となった。
現在位置と最寄り施設のラインを引く部分の実装
ラインを示す GeoJSON-Feature の代入時の実装
ラインを示す変数 routeFeature
に代入する際、@types/geojson
ライブラリ内の Feature
型の定義では properties
が必須となっている。 そのため、null
または {}
を properties
の値に設定する。
const routeFeature: Feature = {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [
userLocation,
(<Point>nearestFeature?._geometry).coordinates,
],
},
// TypeScript 向け: null または {} の properties が存在しないとエラーになる
properties: null
};
地図の 'route' レイヤーのデータ設定を行うときの実装
'route' レイヤーのデータ設定を行う際、map.getSource('route')
で 'route' レイヤーのソースを取得する。getSource() の型定義はジェネリクスになっており、Source
型を拡張した型の値なら問題なく使用できる。Source
型を拡張した GeoJSONSource
型を型定義に代入すれば、戻り値の型は GeoJSONSource
型または undefined
型となる。
map.getSource<GeoJSONSource>('route')?.setData({
type: 'FeatureCollection',
features: [],
});
2024/10 時点では不要となった処理
map.getSource('route')
の型は、ジェネリクスのない Source
または undefined
型だった。そのため、GeoJSONSource
型として型アサーションを行う必要があった。
(<GeoJSONSource>map.getSource('route')).setData({
type: 'FeatureCollection',
features: [],
});
その他の実装
geolocate
イベントのリスナー関数の型
イベントのリスナー関数の第一引数は、ライブラリに添付している型定義は any
型になっている。e.coords
に関しては、GeolocationCoordinates
型の値が入るためそれは設定しておく。
geolocationControl.on('geolocate', (e: {coords: GeolocationCoordinates}) => {
// 位置情報が更新されるたびに発火・userLocationを更新
userLocation = [e.coords.longitude, e.coords.latitude];
});
Discussion