@expo/vector-iconsのアイコン名を型安全に扱う
追記:当初はglyphMapを使う記事として書いていましたが、ComponentPropsを使ったほうが楽そうなので、記事のタイトルを変更しました。記事の大筋は変わってません。
React Nativeでアイコンを扱いたいとき、react-native-vector-iconsを使っている方は多いと思います。このライブラリは、Font AwesomeやIonicons、MaterialIconsやMaterialCommunityIconsなど、多彩なアイコン集をバンドルしています。おかげで、アプリ開発で要求されるアイコンの大部分をこのライブラリで賄える現場もあるのではないでしょうか。
ところで、ExpoでReact Native開発を行う場合には、少し違ったライブラリが用意されています。今回言及する@expo/vector-iconsです。Expoには、アプリの起動時などにJavaScriptコードやアセット(画像など)をサーバーから取得して、自己アップデートする機能(OTA Updates)がある関係で、通常のReact Nativeから見ると少し違ったアセット管理の仕組みを持っています。@expo/vector-iconsは、この少し特殊なアセット管理の仕組みに適応するようにreact-native-vector-iconsをラップしたライブラリです。
本記事では、TypeScriptでアイコン用コンポーネントを扱う際の使い勝手の観点から2つのライブラリを比較した後、@expo/vector-icons特有のTIPSを紹介します。
react-native-vector-iconsのnameはstring型
さて、react-native-vector-iconsの型情報は@types/react-native-vector-iconsで管理されていますが、この型定義において、 name
propsはstring型[1]です。
つまり、次のようなケースで、 "rocket"
という名前の正しさは動かしてみるまでわからないのです。
import Icon from 'react-native-vector-icons/FontAwesome';
const myIcon = <Icon name="rocket" size={30} color="#900" />;
もちろん、注意深く実装すれば問題ありませんが、残念ながら人類は注意深くありません。
型安全を愛する皆さんであれば、 name
が受け入れうる文字列を文字列リテラル型のユニオン型で列挙してほしいですよね。そうすれば、 name
の誤りをtscで検出できます。
@expo/vector-iconsはTypeScript製でnameに型がある
というわけで、実際に name
文字列リテラル型のユニオン型で列挙しているのが、@expo/vector-iconsです。
本来はreact-native-vector-iconsのラッパーでしかなかったのですが、2018〜2019年にExpoプロジェクトがTypeScriptにリライトされた[2]際に@expo/vector-iconsもTypeScriptに書き換えられました[3]。その後、string型だった name
をユニオン型に置き換えるコミットが2020年12月に行われています[4]。例えば、FontAwesomeの型定義は次のようになりました[5]。
declare const _default: import("./createIconSet").Icon<"link" | "search" | "image" | "header" | "key" | "code" | "map" | "table" | "th" | "circle" | "filter" | "stop" | "forward" | "retweet" /* 長いので省略します */>;
export default _default;
どこから持ってきた型情報なのかいまいち不明な部分もあるのですが、react-native-vector-iconsが持っているアイコン名のJSONファイル[6]から生成したような話をどこかで見かけた気がします。
さて、この型定義のおかげで、 name
の誤りを検出できるようになりました。実際の動作を見てみましょう。次のExpo Snackにサンプルコードが置いてあります。
まずは、正しく "rocket"
と入力した場合です。エラーは出ず、画面にもロケットのアイコンが表示されていますね。
これを編集して、 "rocke"
にしてみます。すると、型と値の不整合によるエラーが表示されます(もちろんアイコンは出ません)。
素晴らしい! これこそが型安全を好む私たちが追い求めていた世界です。
というわけで、@expo/vector-iconsにはTypeScriptの観点でreact-native-vector-iconsにはないアドバンテージが存在するのでした。
nameに変数を渡したいときの型をどうするか
さて、 name
にちゃんと型が付いているのは良いことなのですが、少し困るタイミングがあります。次の例のように name
に変数を渡したい場合です。
export default function App() {
let iconName: string; // string型なのでnameの型と噛み合わない
if (/* 条件文 */) {
iconName = 'rocket';
} else {
iconName = 'map';
}
return (
<View style={styles.container}>
<Icon name={iconName} size={30} color="#900" />
{/* ^^^^ Type Error!!!! */}
</View>
);
}
この場合、 name
はエラーになります。 iconName
がstring型で、 name
が求めるユニオン型と噛み合わないからです。
愚直な対応としては、 iconName
の型をユニオン型にしてしまう、という方法があります。
- let iconName: string;
+ let iconName: 'rocket' | 'map';
これなら name
の部分型として成立しますので、型チェックはクリアできます。ただ、すごく面倒です。
本来であれば、アイコン名のユニオン型を表す型情報がexportされていれば嬉しいですね。ただ、残念ながら、前述のFontAwesome.d.tsを見ていただければ分かるとおり、ジェネリクスの型パラメータとしてベタ書きされているような状況ですので、気の利いた型としては公開されていません。
アイコン名の型を頑張って作る
とはいえ定義はされてるんだから、頑張ればどこかから引きずり出せるやろ!ということで、頑張ってみた結果がこちらになります。
import { FontAwesome } from '@expo/vector-icons';
type GlyphNames = ComponentProps<typeof FontAwesome>['name'];
// type GlyphNames = keyof typeof FontAwesome.glyphMap; // 旧実装
各アイコンセットの共通プロパティとして生えている (←旧実装の話)glyphMap
というオブジェクトがアイコン名をキーとして持っているので、 keyof
でキーだけを引っ張り出した形になります。
まずは ComponentProps<typeof FontAwesome>
でFontAwesomeコンポーネントのprops一覧のマップを取り出し、そこから ['name']
で name
propsの型だけを取り出す手法です。( @soh335さんに教えてもらいました🙏 )
GlyphNames
にマウスオーバーしてみると、ちゃんとユニオン型が入っていることがわかります。
というわけで、任意のアイコン名を変数で渡したい場合も、正しい型付けで型注釈を書けるようになりました。
サンプルの最終形は次の通りです。
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { FontAwesome as Icon } from '@expo/vector-icons';
export type GlyphNames = ComponentProps<typeof Icon>['name'];
export default function App() {
let iconName: GlyphNames; // FontAwesomeのすべてのアイコン名を受け付けられる
if (/* 条件文 */) {
iconName = 'rocket';
} else {
iconName = 'map';
}
return (
<View style={styles.container}>
<Icon name={iconName} size={30} color="#900" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
まとめ
@expo/vector-iconsのアイコン名を型安全に扱う方法について紹介しました。
動的にアイコンを切り替えたい場面では重宝するテクニックなので、頭の片隅に置いておくとよさそうです。
-
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/42716053944a355aa3e1804be052112097c63f7b/types/react-native-vector-icons/Icon.d.ts#L26 ↩︎
-
https://github.com/expo/vector-icons/commit/48761c0222581c3411496cbc94a3f09cae5caad8 ↩︎
-
https://github.com/expo/vector-icons/blob/630e2bb16cd255b19ebad7afd8b14bfdcf67b618/build/FontAwesome.d.ts ↩︎
-
https://github.com/expo/vector-icons/blob/630e2bb16cd255b19ebad7afd8b14bfdcf67b618/build/vendor/react-native-vector-icons/glyphmaps ↩︎
Discussion