Vuetify3のカスタムアイコン名に型定義を効かせてみた

こんにちは、GMOメイクショップ フロントエンドエンジニアの原田です。 Vuetify3のカスタムアイコン名の入力ミスを無くすため、型定義を改善してみました。この記事では、その定義方法をご紹介します。

課題

現在開発している次世代ECフロントエンドではVuetify3を採用しており、カスタムアイコンを登録して利用しています。 独自のデザイン規約があるため、アイコンの表示には、下記のようなラッパーコンポーネントを利用しています。

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  // TODO: 今回の課題
  name: string
  color?: string
  disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
  color: 'primary',
  disabled: undefined,
})
</script>

<template>
  <v-icon class="next-ec-icon" :color="props.disabled ? 'disabled' : props.color" >
    {{ props.name }}
  </v-icon>
</template>

<style scoped lang="scss">
.next-ec-icon {
  svg {
    transition: all 0.2s ease-in-out;
  }
}
</style>

このコンポーネントのnameプロパティは実際には一定のアイコン名しか受け付けないにも関わらず、型がstringであるため何でも入ってしまいます。 アイコン名の入力ミスを防ぐために、より厳密な型定義を検討しました。

方法

方法1: $${string}

まず考えたのは、アイコン名を$で始まる文字列に限定する方法です。これにより、一定の入力ミスを防ぐことができます。

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  name: `$${string}`
  color?: string
  disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
  color: 'primary',
  disabled: undefined,
})
</script>
import NIcon from '@/components/objects'

const iconNameOk = "$info"
const iconNameNg = "info"

<NIcon :name="iconNameOk" /> // 型エラーなし
<NIcon :name="iconNameNg" /> // Argument of type '"info"' is not assignable to parameter of type '`$${string}`'.ts(2345)

方法2: vuetify.tsに定義している アイコン一覧から 型定義を作る

方法1だけでは$で始まる文字列であれば何でも入ってしまいます。もっと正確な型定義を行うためには、Vuetify3のアイコン定義ファイル(vuetify.ts)を利用できそうです。 この場面では、as const satisfies と ユニオン型を使うと良さそうです。

as const satisfies とは? TypeScript 4.9より導入された構文で、特定の型を満たした定数型を作成します。 これにより、string型でなく、testのように特定の文字列だけが入る厳密な型を生成できます。

type T = {
  key: string
  value: string
}

const hoge: T = {
  key: "test",
  value: "test"
}
// hoge: T

const fuga = {
  key: "test",
  value: "test"
} as const satisfies T
// fuga: { readonly key: "test", readonly value: "test" }

ユニオン型とは? "A または B のどちらか" のような 複数の型のいずれかであることを示す型定義です。

type AorB = 'A' | 'B'

const a: AorB = "A" // OK
const b: AorB = "B" // OK
const c: AorB = "C" // NG

ユニオン型は手動で書くだけでなく、下記のように定数配列から自動で生成することもできます。

const users = [
  {id: 1201, name: "KRN"},
  {id: 1204, name: "CN"},
] as const

type UserNames = (typeof users)[number]["name"] // 'KRN' | 'CN'

自動作成した型定義を更に加工することで、特殊な型も作ることができます。

type PrefixString<T extends string> = T extends `${infer P}${string}` ? P : never;
type UserNamePrefixes = PrefixString<UserNames> // `K` | `C`

const userNamePrefixK: UserNamePrefixes = "K" // OK
const userNamePrefixC: UserNamePrefixes = "C" // OK
const userNamePrefixT: UserNamePrefixes = "T" // NG

作成した型定義

as const satisfies と ユニオン型を使い、アイコン名一覧となるNextEcIcons型を生成しました。

import type { IconAliases, IconOptions } from 'vuetify'

import iconMakeRepeater from '@/components/icon/IconMakeRepeater.vue'
import iconPin from '@/components/icon/IconPin.vue'

/** アイコン一覧定義 */
const customAliases = {
  // 独自アイコン
  makeRepeater: iconMakeRepeater,
  pin: iconPin,
  // (以下省略)
} as const satisfies IconAliases

/** 先頭に$を付けた文字列型を生成 */
type PrependDollarSign<T> = {
  [K in keyof T as `$${string & K}`]: T[K]
}

/** アイコン名一覧型 */
export type NextEcIcons = keyof PrependDollarSign<typeof customAliases>

/** Vuetify3用アイコン定義 */
const customIcons: IconOptions = {
  defaultSet: 'mdi',
  aliases: customAliases,
  sets: {
    mdi,
  },
}

// (以下省略)

生成したNextEcIcons型をNIcon.vueでインポートし、name Propに使用します。

<script setup lang="ts">
import { computed } from 'vue'

import type { NextEcIcons } from '@/plugins/vuetify/customIcons'

interface Props {
  name: NextEcIcons
  color?: string
  disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
  color: 'primary',
  disabled: undefined,
})
</script>

これにより、型定義が厳密になり、実際に定義されているアイコン名のみを受け付けるようになりました🎉

import NIcon from '@/components/objects'

const iconNameOk = "$makeRepeater"
const iconNameNg = "$makeRepeat"

<NIcon :name="iconNameOk" /> // 型エラーなし
<NIcon :name="iconNameNg" /> // Argument of type '"$makeRepeat"' is not assignable to parameter of type '`$makeRepeater| $pin`'.ts(2345)

型が厳密に絞られたので、入力補完も効くようになります🎉🎉🎉

まとめ

as const satisfiesとユニオン型を使い、Vuetifyのカスタムアイコン定数に定義済みのアイコン名だけが入る型定義を作成しました。 これにより存在するアイコン名だけを入力に受け付けるようになり、ミス無く快適にアイコン名を指定できるようになりました。 今後もTypeScriptを活用し、よりミスを起こしづらい開発環境を整えて行きたいと思います。

参考