紡Book の勉強ノートに戻る
技術ブログ2026.04.19

同じコンポーネントに別の見た目を足したら、片方が真っ白になった — デフォルト引数を置いてはいけない場面

コードレビューTypeScriptReact

AI に書いてもらった variant コンポーネントに見た目(variant)を1つ足したら、型が合わずビルドが落ちた。原因を後から追って腑に落ちた「デフォルト値の型ずれ」を、進行形の学習ログとして残す。コードは要点だけ抜き出した形にしている。


この記事でわかること

  • variant ごとに型が違う prop にデフォルト引数を置くと何が起きるか
  • なぜそれが危険なのか(ビルド壊滅パターン)
  • 「デフォルトを置かず必須にする」で構造的に防ぐ考え方

何が起きたか

綴Photo の LP を別パターンの見せ方でも出せるようにしたくて、classic 用に AI に書いてもらったコンポーネントに、後から variant="pop" を足した。<MultiVariantSection variant="pop" /> と呼んで表示してみると──セクションが丸ごと空白。ビルド(next build)を通すと TypeScript のコンパイルエラーで止まります。

原因は単純で、pop 専用のプロパティに、classic 用のデフォルト値が刺さっていた。

前提:呼び出し方が classic と pop で違う

このコンポーネントは pairs プロパティでデータを受け取る作りで、classic も pop も pairs を読んで表示します。違うのは呼び出し側で pairs を渡しているかどうか:

tsx
// 元々の使い方(classic だけ)
<MultiVariantSection variant="classic" pairs={CLASSIC_PAIRS} />

ところが pop を後から足した時、呼び出し側はこう書いた:

tsx
<MultiVariantSection variant="pop" />   // ← pairs を省略

やりがちな書き方(これがビルドを落とす)

上の状況をコードで見るとこうなります(要点だけ抜き出している)。

tsx
type PopPair = {
    leftTag: string;
    leftText: string;
    rightTag: string;
    rightText: string;
};
 
type Props = {
    variant?: 'classic' | 'pop';
    pairs?: PopPair[];  // ← 型は PopPair[]
};
 
// CLASSIC_PAIRS は { voice, answer }[] 型(PopPair と全く別の型)
const CLASSIC_PAIRS = [
    { voice: '...', answer: '...' },
    // ...
];
 
export default function MultiVariantSection({
    variant = 'classic',
    pairs = CLASSIC_PAIRS,  // ← ここ! PopPair[] に { voice, answer }[] を渡している
}: Props) {
    if (variant === 'pop') {
        return (
            <>
                {pairs.map((pair) => (
                    <Bubble tag={pair.leftTag}>
                        {pair.leftText}
                    </Bubble>
                ))}
            </>
        );
    }
    // ...
}
CLASSIC_PAIRS はソースの const。実行時はメモリ。DB にも prefs にも保存しない。呼び出し側関数の pairs描画classic渡すvariant="classic"pairs を明示して渡す渡された値を使うCLASSIC_PAIRSvoice / answer項目があるので描けるpop省略variant="pop"pairs を書かないデフォルトが刺さるCLASSIC_PAIRS のままleftTag を探す項目が無く空白省略したときだけデフォルトが効く。中身は classic 用の const であり、保存しない。

なぜ危険か

実体はソースの CLASSIC_PAIRS。実行時はメモリ。DB にも prefs にも保存しない。型の宣言PopPair = leftTag / leftText実体CLASSIC_PAIRS = voice / answer弾く経路型が合わないソース上の宣言が弾くnext build 失敗組み立てが止まる公開が止まるデプロイできないすり抜ける経路キャストで通す型チェックを外すleftTag は undefined項目が無いセクションが空白console は出ない

next build は公開できる形に組み立てる工程であり、ここで止まるとデプロイ=公開ができません。すり抜けた経路はエラーが console に出ないケースがあり、「なんか表示されてないな」程度で気づかれにくいです。

避け方:デフォルト引数を取らず、呼び出し側で必ず渡す

「デフォルトを POP_PAIRS に変える」でも目の前のバグは消えます。けれど variant ごとに型が違う prop にデフォルトを置くこと自体が地雷で、新しい variant を足すたびに同じ事故が起きます。

筋がいいのはデフォルト引数を取らず、呼び出し側で必ず渡す形にすること。

tsx
// 関数側:デフォルト引数を消す
export default function MultiVariantSection({
    variant = 'classic',
    pairs,                // ← デフォルト値を置かない
}: Props) {
tsx
// 呼び出し側:variant に対応する pairs を毎回明示
<MultiVariantSection variant="classic" pairs={CLASSIC_PAIRS} />
<MultiVariantSection variant="pop" pairs={POP_PAIRS} />
CLASSIC_PAIRS も POP_PAIRS もソースの const。実行時はメモリ。保存しない。関数の引数デフォルトを置かない省略する箱が無い省略時に埋める値を持たない呼び出し側classic に CLASSIC_PAIRSpop に POP_PAIRS毎回、その見た目用を渡す型の宣言? を外して必須省略すると弾かれるソース上の約束。保存しないデフォルト経由で classic 用が pop に流れる箱が無い。定数はソースにあり、DB にも prefs にも無い。

新しい variant(minimal, dark ...)が増えても、その都度「専用のデータを明示的に渡す」運用が続くので、デフォルト経由でデータが混線する事故が構造的に起きません。

学びポイント

  • デフォルト引数は呼び出し側が省略したときだけ効きます。普段は明示的に pairs を渡していてバグが顔を出さず、variant を足してデフォルトに頼った瞬間に初めて踏む ── だから「うっかり」ではなく、この設計なら誰でもハマる方向の落とし穴
  • variant ごとに型が違う prop には、そもそもデフォルト引数を置きません。「片方の variant 用」のデフォルトは、別 variant が増えるたびに同じ事故の種になります
  • 代わりに型側でオプショナルを外して必須にし、呼び出し側で必ず渡させます。TypeScript が「省略禁止」を強制してくれるので、同じ間違いを構造的に防げます
  • classic と pop でデータ型が違うなら、pairs プロパティの型を union(=「この型 or あの型」とどちらでもOKと宣言する)にするか variant ごとに分離するか、設計段階で決めておくと後のミスが減ります

おわりに

直すのは数文字でも、踏むと影響は「ビルド失敗 + セクション丸ごと空白」という破壊力。コード量が少ない変更ほど、デフォルト値やプロパティ名の見落としが起きやすいです。

綴Photo は実装を AI に書いてもらう進め方をしていて、variant で受け取る型が違うこのセクションもそうやって組んだ。最初は「デフォルトを片方用に置いたまま、もう片方を呼んだ」ことに気づかず、ビルドが落ちてから後追いで腑に落ちた。直す段で「そもそもデフォルトを置かない」に倒せば、新しい variant を足すたびに同じ事故を踏むルートが構造的に塞がる ── 目の前のバグを消すだけでなく、設計を一段固くする方向で直す視点を持ちたい。


この記事は2026年4月時点の自分の理解で書いています。TypeScript の型推論や React の variant パターン自体は安定した領域だが、shadcn/ui や CVA など variant を扱う周辺ツールはアップデートが入ることがあるので、最新の公式ドキュメントも合わせて参照してください。