Mirai Translate TECH BLOG

株式会社みらい翻訳のテックブログ

TypeScriptで型に? (Optional Property) を使わないほうがいいとき

こんにちは。みらい翻訳でフロントエンジニアをしているWillと申します。

今回は、コードレビューでたまに指摘する、TypeScriptのOptional Propertyを使わないほうが良い場面を紹介します。

Optional Propertyについてのおさらい

まず、Optional Propertyについて軽く確認します。

type User = {
    id: number;
    name: string;
    nickname: string?;
}

この時、User型は、idとnameプロパティは必須です。一方でnicknameプロパティはオプショナルとなり、その存在は必須ではありません。

Optional Propertyに関するドキュメント TypeScript: Handbook - Interfaces

TypeScriptの基本的な機能であり、みなさんも様々な場面でOptional Propertyを使われているかと思います。

今回は、このOptional Propertyではなく、ユニオン型を使ったほうがよい場合を紹介します。

アラートコンポーネントでの例

例えば以下のような条件のReactコンポーネントを考えます。

  • アラートを表示するコンポーネント
    • 「お知らせ」と「警告」の2種類のタイプがある
    • どちらのタイプにもOKボタンがある
    • 警告の場合は、助けを求めるためのヘルプボタンがある

Optional Propertyを使った実装

以下のように実装しました。

const Alert = (param:
    {
        type: 'warning' | 'notice';
        text: string;
        onOkButtonClick: () => void;
        onHelpButtonClick?: () => void
    }
) => {
    return (
        <div>
            <p>
                {param.text}
            </p>
            <button onClick={param.onOkButtonClick}>OK</button>
            {
                param.type === 'warning'
                && <button onClick={param.onHelpButtonClick}>HELP</button>
            }
        </div>
    );
};

onHelpButtonClickコールバックは、Optional Propertyを使って実装されています。

それでは実際に使ってみます。

<Alert type="notice" text="お知らせ" onOkButtonClick={()=>{}}/>

「お知らせ」は問題ないですね。

しかし、

<Alert type="warning" text="警告" onOkButtonClick={()=>{}}/>

「警告」はonHelpButtonClickの実装が要件的には必須であるにも関わらず、型で表現されていないため、 上記のようなonHelpButtonClickの実装が漏れていても型エラーになりません。

これでは、このコンポーネントを利用する際に誤った実装をしてしまう可能性があります。

ユニオン型を使った実装

onHelpButtonClickにOptional Propertyを使っては、要件を正確に型に落とし込むことができません。

では、ユニオン型を使って表現してみましょう。

const Alert = (param: {
    type: 'warning';
    text: string;
    onOkButtonClick: () => void;
    onHelpButtonClick: () => void;
} | {
    type: 'notice';
    text: string;
    onOkButtonClick: () => void;
}) => {
    return (
        <div>
            <p>{param.text}</p>
            <button onClick={param.onOkButtonClick}>OK</button>
            {param.type === 'warning' && <button onClick={param.onHelpButtonClick}>HELP</button>}
        </div>
    );
};

タグ付きユニオン を使って表現します。

'warning'の場合、onHelpButtonClickが必須であることが型レベルで定義されます。

先程の「警告」タイプで、onHelpButtonClickの実装が漏れていた以下のコードはタイプチェックが通りません。

<Alert type="warning" text="警告" onOkButtonClick={() => {}} />; 
 TS2322: Type '{ type: "warning"; text: string; onOkButtonClick: () => void; }' is not assignable to type 'IntrinsicAttributes & ({ type: "warning"; text: string; onOkButtonClick: () => void; onHelpButtonClick: () => void; } | { type: "notice"; text: string; onOkButtonClick: () => void; })'.   Property 'onHelpButtonClick' is missing in type '{ type: "warning"; text: string; onOkButtonClick: () => void; }' but required in type '{ type: "warning"; text: string; onOkButtonClick: () => void; onHelpButtonClick: () => void; }'.

これで、間違ってヘルプボタンが反応しないアラートを作ってしまう危険性がなくなりました。

おまけ

共通のプロパティは以下のように交差型を使って共通化しておくとより分かりやすいです。

type Common = {
    text: string;
    onOkButtonClick: () => void;
};
const Alert = (param: {
    type: 'warning';
    onHelpButtonClick: () => void;
} & Common | {
    type: 'notice';
} & Common) => {

...

まとめ

Optional Propertyを使わないほうがよい場面について紹介しました。

例として割と極端な例を取り扱ったため、分かりやすかったと思いますが、実際のコードでは気づきにくい場合もあります。

型によるドキュメントを意識して、安全なコーディングをしていきましょう。

みらい翻訳ではエンジニアを募集しています

みらい翻訳では、フロントエンジニアを募集しています。

ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。(フロントエンドエンジニア以外も募集中です)

miraitranslate.com