Slide 1

Slide 1 text

Polymorphic Components パターン で作る、型安全でセマンティックな UI コンポーネント 2026.5.23 TSKaigi2026 ryo ( @ryohiy_ )

Slide 2

Slide 2 text

株式会社Belong FEE/SWE 最近は Three.js を好きで触っています ryo (@ryohiy_) 自己紹介

Slide 3

Slide 3 text

例えば、以下のような Text コンポーネントを実装 デザインシステム コンポーネント実装時の課題 export function Text({ children, ...props }) { return {children} }

Slide 4

Slide 4 text

例えば、以下のような Text コンポーネントを実装 デザインシステム コンポーネント実装時の課題 export function Text({ children, ...props }) { return {children} } Text を使うと常に span になる -> ❌ Semantic HTML 呼び出し側で要素を自由に宣言出来るようにしたい

Slide 5

Slide 5 text

デザインシステムのコンポーネント実装やUI ライブラリで利用 するデザインパターン コンポーネントの要素を呼び出し側から宣言する事を 出来るようにする Polymorphic Components とは

Slide 6

Slide 6 text

実装例 export function Text({ as, children, ...props }) { const Component = as || 'span' return {children} } Text コンポーネントを Polymorphic にすると

Slide 7

Slide 7 text

実装例 export function Text({ as, children, ...props }) { const Component = as || 'span' return {children} } Text コンポーネントを Polymorphic にすると as として HTML Tag の指定を受け取り、jsx の type として利用する ※ 型 assertion の 「as」とは異なるものです。これは Polymorphic Components の as prop パターン

Slide 8

Slide 8 text

実装例 ページタイトル 2026年5月23日 利用側:

ページタイトル

レンダリング後:

Slide 9

Slide 9 text

実装例 利用側: レンダリング後: ❌ as= hoge ❌ as=h1 , dateTime=”2026-05-23” ページタイトル 2026年5月23日

ページタイトル

Slide 10

Slide 10 text

実装例 利用側: レンダリング後: ❌ as= hoge ❌ as=h1 , dateTime=”2026-05-23” ページタイトル 2026年5月23日 コードを書いている時に気づけるようにしたい => 型定義を活用する

ページタイトル

Slide 11

Slide 11 text

実装① cva でデザイントークンを定義 const textVariants = cva('', { variants: { size: { md: 'text-base', lg: 'text-3xl', }, weight: { normal: 'font-normal', bold: 'font-bold', }, } })

Slide 12

Slide 12 text

実装② 自前の props を定義 interface TextOwnProps extends VariantProps { as?: T }

Slide 13

Slide 13 text

実装② 自前の props を定義 interface TextOwnProps extends VariantProps { as?: T }

Slide 14

Slide 14 text

実装③ HTML 要素の props と合成 type TextProps = TextOwnProps & Omit, keyof TextOwnProps | 'className'> の HTML 属性を取得

Slide 15

Slide 15 text

実装③ HTML 要素の props と合成 type TextProps = TextOwnProps & Omit, keyof TextOwnProps | 'className'> 自前 prop と衝突するキーを除外

Slide 16

Slide 16 text

実装④ コンポーネントを実装 export function Text({ as, size, weight, children, ...props }: TextProps) { const Component = as || 'span' return ( {children} )}

Slide 17

Slide 17 text

実装④ コンポーネントを実装 export function Text({ as, size, weight, children, ...props }: TextProps) { const Component = as || 'span' return ( {children} )} ②と同じ形

Slide 18

Slide 18 text

実装④ コンポーネントを実装 export function Text({ as, size, weight, children, ...props }: TextProps) { const Component = as || 'span' return ( {children} )} ③の実装

Slide 19

Slide 19 text

利用例 ページタイトル // =>

ページタイトル

本文テキスト // =>

本文テキスト

2026年5月23日 // => OK

Slide 20

Slide 20 text

利用例 ページタイトル // =>

ページタイトル

本文テキスト // =>

本文テキスト

2026年5月23日 // => OK

Slide 21

Slide 21 text

利用例 ページタイトル // =>

ページタイトル

本文テキスト // =>

本文テキスト

2026年5月23日 // => OK

Slide 22

Slide 22 text

利用例 ページタイトル // =>

ページタイトル

本文テキスト // =>

本文テキスト

2026年5月23日 // => OK

Slide 23

Slide 23 text

利用例 ページタイトル // => ElementType で type check error 本文テキスト // => size は sm を持たないため type check error 2026年5月23日 // => h1 は dateTime 属性を持たないため type check error NG

Slide 24

Slide 24 text

利用例 ページタイトル // => ElementType で type check error 本文テキスト // => size は sm を持たないため type check error 2026年5月23日 // => h1 は dateTime 属性を持たないため type check error NG

Slide 25

Slide 25 text

利用例 ページタイトル // => ElementType で type check error 本文テキスト // => size は sm を持たないため type check error 2026年5月23日 // => h1 は dateTime 属性を持たないため type check error NG

Slide 26

Slide 26 text

利用例 ページタイトル // => ElementType で type check error 本文テキスト // => size は sm を持たない為 type check error 2026年5月23日 // => h1 は dateTime 属性を持たないため type check error NG

Slide 27

Slide 27 text

参考資料 ● https://github.com/shadcn-ui/ui ● https://www.components.build/polymorphism ● https://web.dev/learn/html/semantic-html ● https://developer.mozilla.org/ja/docs/Glossary/Semantics ● https://developer.mozilla.org/ja/docs/Learn_web_development/Core/ Accessibility/HTML ● https://zenn.dev/tsuboi/articles/8abddb1ae3038f

Slide 28

Slide 28 text

おわりに 本題のブログ Belong 採用