Upgrade to Pro — share decks privately, control downloads, hide ads and more …

childrenの順序まで型で縛る ── Branded Typesで実践するJSXの構造安全

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Mahito Mahito
May 23, 2026
310

childrenの順序まで型で縛る ── Branded Typesで実践するJSXの構造安全

Avatar for Mahito

Mahito

May 23, 2026

Transcript

  1. 藤野 眞人 Sansan株式会社 技術本部 Eight Engineering Unit Product Devグループ -

    2020年に新卒入社 - 名刺アプリ「Eight」のWeb開発全般を担当しています - 最近は野鳥観察がマイブーム
  2. JSXの中身は”Black box” 公式ドキュメントにも記載されている By default the result of a JSX

    expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box. 出典元:TypeScript - The JSX function return type この前提とどう戦っていくか
  3. function CardHeader(p: HeaderProps) { return <header><h2>{p.title}</h2></header>; } function CardBody(p: BodyProps)

    { return <section>{p.children}</section>; } type CardHeaderEl = ReactElement<HeaderProps, typeof CardHeader>; type CardBodyEl = ReactElement<BodyProps, typeof CardBody>; type CardProps = { children: readonly [CardHeaderEl, CardBodyEl]; }; ReactElement<P, typeof Component> で型定義し、childrenをtupleにする まずは素直に書いてみる
  4. が、全て通ってしまう。。。 不正な構造なのに、コンパイルエラーにならない例 <Card> <CardBody>x</CardBody> // ✅ 通る (順序入れ替え) <CardHeader title="y"/>

    // ✅ 通る </Card> <Card> <div>nope</div> // ✅ これも通る <span>...</span> // ✅ </Card> → JSX式でのreturnは、一律 JSX.Element に変換されてしまうため
  5. Branded Typesを応用する 1. Branded Types の定義 // 馴染みの形 (ID の取り違え防止)

    declare const __brand: unique symbol; type Brand<T, B extends string> = T & { readonly [__brand]: B }; type PersonId = Brand<number, 'PersonId'>; type CardId = Brand<number, 'CardId'>; type CardHeaderEl = Brand< ReactElement<CardHeaderProps>, 'CardHeader' >; 2. 関数コンポーネントへの適用 function CardHeader(p: HeaderProps) { return ( <header><h2>{p.title}</h2></header> ) as unknown as CardHeaderEl; } ポイント: - 関数の戻り値を CardHeaderEl にキャスト - これにより JSX.Element ではなく 特定の型として保持される - 構造的な型の安全性を担保することが 可能になる
  6. 呼び出し方のポイント ✅ OK // 関数として直接実行する <Card> {CardHeader({ title: 'Profile' })}

    {CardBody({ children: 'Hello' })} </Card> ❌ NG // コンポーネントとして呼び出す <Card> <CardHeader title='Profile' /> <CardBody>Hello</CardBody> </Card> 関数呼び出しの形式で子要素を作成 → JSX式でJSX.Elementに潰される事を回避し、Branded Typesの型情報を保持
  7. 今回の記法のメリット・デメリット メリット - children の順序・個数・種別を型のみで縛れる デメリット - DevToolsに子コンポーネント名が表示されない - Hooks

    / Suspense / Context等が使えないので、Cardのような表示専用のコンポーネントに限定される → 向き・不向きがはっきりしており、採用前に把握するべきトレードオフがある
  8. TypeScriptの今後に期待 Wes Wigham 氏による 2018年の提起 (Issue #21699) JSX 式の型を、JSX.Element ではなく

    factory function の戻り値から解決すべき、という提案。 2019年の実装 PR はクローズ 理由はパフォーマンスの低下: - 型チェック時間・メモリ消費が増加 - JSXのネスト構造が、型推論にとって 構造的に重い TypeScriptはパフォーマンス改善のネイティブ実装に取り組んでおり、今後の改善に期待 ✨
  9. まとめ - Brand を被せると JSX.Element には代入できなくなる - = children の順序・個数・種別が型で縛れる

    - ユースケースは表示専用のコンポーネント - 例: 自社のデザインシステム - Issue #21699 が解決し、今後の TSの進化に期待 ✨