OSSにコントリビュートしたのとzodのすゝめ

この記事は、 Calendar for 東京電機大学 | Advent Calendar 2022 - Qiita の1日目です。

皆さん、OSS活動していますか?私は、最近、ealush/emoji-picker-react: React Emoji Picker に対してバグ修正のPRを投げました。

github.com

この記事では、このバグについての説明と、そういったバグをなくすための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に指定するのですが、そのユニコードが存在しない場合に内部でエラーが発生してしまいます。

github.com

さらに、Next.jsでuseSWRを使用してAPIから絵文字情報を取得してからこのunifiedに代入するみたいな処理を追加するとよくわからないのですが本番環境でのみ確率的に発生するバグも出現してしまいました。

修正する

そのため、実装コードを色々眺めていたところ面白い実装を発見しました。

 export function asEmoji(emoji: DataEmoji | undefined | null): DataEmoji { 
   return emoji as DataEmoji; 
 } 

魔法の関数です。この関数にundefinednullを渡してもDataEmojiいい感じに変換してくれます。 そんなことはなく、TypeScriptの取り柄である型情報をasで上書きしてしまい隠してしまっています。
そのため、この関数の戻り値を使用しているところを探すと、

export function emojiNames(emoji: DataEmoji): string[] {
  return emoji[EmojiProperties.name] ?? [];
}

https://github.com/ealush/emoji-picker-react/blob/4daf6d0e0311a1fa3e24b3fe7551f89f9d84db6e/src/dataUtils/emojiSelectors.ts#L18-L20

と、ここで使用していました。DataEmojiは、

export type DataEmoji = {
  n: string[];
  u: string;
  v?: string[];
  a: string;
};

https://github.com/ealush/emoji-picker-react/blob/4daf6d0e0311a1fa3e24b3fe7551f89f9d84db6e/src/dataUtils/DataTypes.ts

と定義されているのでemoji[EmojiProperties.name] ?? []は正しいです。しかし、emojiundefinednullの場合、エラーが発生していまいます。これが、バグの原因でした。

修正は、特に難しいことはしていなくて、asEmoji関数を削除し、emojiundefinednullの場合はそのまま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というライブラリを触ってみてはいかがでしょうか?