Vue.js+TypeScriptでGoogleMapを使う

Vue.js公式のcookbookにexampleが乗ってる

  • https://jp.vuejs.org/v2/cookbook/practical-use-of-scoped-slots.html
  • slotを使ってGoogle Mapをロードする用のコンポーネントを作成
  • scoped slotでgoogle, map propertyを公開する
  • 親コンポーネントで、slotのpropertyを使ってmarkerやpolylineを描画するのに使う
  • markerコンポーネントを作ってpropsにgoogleやmapを渡すことで使うことができる

TypeScript

https://developers.google.com/maps/documentation/javascript/using-typescript

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/googlemaps/ DefinitelyTypedにあるものを使う

Vue.js

GoogleMapLoader.vue

<template>
  <div>
    <div ref="googleMap" class="google-map h-full"></div>
    <template v-if="google && map">
      <slot :google="google" :map="map" />
    </template>
  </div>
</template>
 
<script lang="ts">
import { Loader } from '@googlemaps/js-api-loader'
import { Vue, Component, Prop } from 'vue-property-decorator'
 
@Component
export default class GoogleMapLoader extends Vue {
  @Prop({ type: Object })
  mapConfig?: google.maps.MapOptions
 
  @Prop({ type: String })
  apiKey?: string
 
  @Prop({ type: String })
  apiVersion?: string
 
  google: typeof google | null = null
  map: google.maps.Map | null = null
 
  $refs!: {
    googleMap: Element
  }
 
  mounted() {
    const loader = new Loader({
      apiKey: this.apiKey || this.$config.googleMapsApi.key!,
      version: this.apiVersion || this.$config.googleMapsApi.version!,
    })
 
    loader
      .load()
      .then(() => {
        this.google = window.google
        const mapContainer = this.$refs.googleMap
        this.map = new this.google.maps.Map(mapContainer, {
          ...this.mapConfig,
        })
 
        this.$emit('map-loaded')
      })
      .catch((e) => {
        this.$emit('failed-map-loading')
      })
  }
 
  // Mapを操作するためにメソッドを公開
  // this.$refs.map.setCenter(latLng) のように呼び出す
  setCenter(latLng: google.maps.LatLng) {
    if (!this.google || !this.map) {
      return
    }
    this.map.setCenter(latLng)
  }
}
</script>
 

GoogleMapMarker.vue

<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
 
@Component
export default class GoogleMapMarker extends Vue {
  @Prop({ type: Object, required: true })
  google!: typeof google
 
  @Prop({ type: Object, required: true })
  map!: google.maps.Map
 
  @Prop({ type: String, required: true })
  url!: string
 
  @Prop({ type: Object, required: true })
  position!: google.maps.LatLng
 
  marker: google.maps.Marker | null = null
 
  mounted() {
    this.marker = new this.google.maps.Marker({
      position: this.position,
      map: this.map,
      icon: {
        url: this.url,
      },
    })
  }
}
</script>

困ったこと

@types/googlemapsの定義がnamespaceになっているのでそのままだとgoogle型が使えない

declare namespace google.maps {
	...
}
解決策

typeof google で型宣言する

  // eslint-disable-next-line no-undef
  let google: typeof google | null = null

VeturでNull-safety operatorが怒られる(バグ?)

  initializeMap() {
    const mapContainer = this.$refs.googleMap
    this.map = new this.google?.maps.Map(mapContainer, this.mapConfig)
  }
This expression is not constructable.  
Type 'typeof google' has no construct signatures.Vetur(2351)
解決策

あらかじめnull判定をしておく

  initializeMap() {
    const mapContainer = this.$refs.googleMap
    if (!this.google) {
      return
    }
    this.map = new this.google.maps.Map(mapContainer, this.mapConfig)
  }

その他ドキュメント

https://developers.google.com/maps/documentation/javascript/examples/event-simple