この記事は、 Calendar for 東京電機大学 | Advent Calendar 2022 - Qiita の1日目です。
皆さん、OSS活動していますか?私は、最近、ealush/emoji-picker-react: React Emoji Picker に対してバグ修正のPRを投げました。
この記事では、このバグについての説明と、そういったバグをなくすためのzodという型ガードライブラリの紹介をします。
バグの詳細
emoji-picker-react
は、Reactで絵文字ピッカーを実装するためのnpmライブラリです。READMEを見るとイメージがあるのですが、文字で説明するとDiscordやSlackでチャット横の絵文字ボタンを押すと絵文字一覧が出てきて検索したりできるアレです。
で、このライブラリではEmoji
コンポーネントという機種依存である絵文字をAppleスタイルやTwitterスタイルなどそれぞれ別の絵文字で表示できるものがあります。内部では、絵文字のユニコードがファイル名になっている画像を持ってきている感じです。
使い方はREADMEにありますが、以下のようになります。
import { Emoji, EmojiStyle } from 'emoji-picker-react'; export function MyApp() { return ( <p> My Favorite emoji is: <Emoji unified="1f423" size="25" /> </p> ); }
ここで、表示させたい絵文字のユニコードをunified
に指定するのですが、そのユニコードが存在しない場合に内部でエラーが発生してしまいます。
さらに、Next.jsでuseSWR
を使用してAPIから絵文字情報を取得してからこのunified
に代入するみたいな処理を追加するとよくわからないのですが本番環境でのみ確率的に発生するバグも出現してしまいました。
🙄
— HIBIKI CUBE (@hibiki_cube) 2022年11月23日
(再読み込みすれば直る) pic.twitter.com/u1RnPwwru0
修正する
そのため、実装コードを色々眺めていたところ面白い実装を発見しました。
export function asEmoji(emoji: DataEmoji | undefined | null): DataEmoji { return emoji as DataEmoji; }
魔法の関数です。この関数にundefined
やnull
を渡してもDataEmoji
にいい感じに変換してくれます。 そんなことはなく、TypeScriptの取り柄である型情報をas
で上書きしてしまい隠してしまっています。
そのため、この関数の戻り値を使用しているところを探すと、
export function emojiNames(emoji: DataEmoji): string[] { return emoji[EmojiProperties.name] ?? []; }
と、ここで使用していました。DataEmoji
は、
export type DataEmoji = { n: string[]; u: string; v?: string[]; a: string; };
と定義されているのでemoji[EmojiProperties.name] ?? []
は正しいです。しかし、emoji
がundefined
やnull
の場合、エラーが発生していまいます。これが、バグの原因でした。
修正は、特に難しいことはしていなくて、asEmoji
関数を削除し、emoji
がundefined
かnull
の場合はそのままreturn null;
をするように変更しました。
zodの紹介
さて、今回のバグは無理やり型キャストを行っていたため発生したものですが、APIをfetchしてレスポンスのjsonをパースするといったことをTypeScriptで書くときに型キャストをするといったことも1つ間違えば今回と同じようなバグが発生するかもしれません。
そういったケースにzodというnpmライブラリを使ってみてください。zodは簡単に説明するとTypeScriptの型ガードを高性能 & 簡単化したものです。
例えば、以下のようなjsonオブジェクトを用意します。
{ name: "cateiru", age: 21, twitter: "https://twitter.com/cateiru" }
これをTypeScriptで使用する場合、JSON.parse()
を使うかと思いますが戻り値はany
です。ここで、zodを使用すると以下のように書くことができます。
import {z} from 'zod'; const data = ""; // string json data const dataSchema = z.object({ name: z.string(), age: z.number(), twitter: z.string().url(), }); // パースに失敗した場合はErrorをthrowする const parsedData = dataSchema.parse(JSON.parse(data)); // パースに失敗した場合はsuccessがfalse const {success, data} = dataSchema.safeParse(JSON.parse(data)); if (!success) { ... }
このようにzodでスキーマを定義してあげることで簡単にパースしてくれて大変便利です。
ぜひこの機会にzodというライブラリを触ってみてはいかがでしょうか?