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

UIパーツの設計を「型」から読み解く 〜TSKaigiのセッションから得た学び〜

UIパーツの設計を「型」から読み解く 〜TSKaigiのセッションから得た学び〜

TSKaigi 2026事後勉強会の登壇資料です。
https://smarthr.connpass.com/event/392342/

Avatar for yud0uhu

yud0uhu

June 25, 2026

More Decks by yud0uhu

Other Decks in Technology

Transcript

  1. Before Before 1 2 3 4 5 6 7 8

    9 10 11 // 「トグルするアイコン」と // 「トグルしないアイコン」の責務が混在している type Props = { isShrinking: boolean; iconComponent: React.ReactNode; // トグルアイコンなのに、トグルしないアイコンが存在する previousIconComponent?: React.ReactNode; // アニメーションをさせるためだけに存在する handleShrinkComplete: () => void; handleGrowComplete: () => void; };
  2. After 「discriminated union」でUIの振る舞いを型に閉じ込める 1 2 3 4 5 6 7

    8 9 10 11 12 13 type ToggleProps = BaseProps & { type: "toggle"; isToggled: boolean; fromIcon: React.ReactNode; toIcon: React.ReactNode; }; type NonToggleProps = BaseProps & { type: "non-toggle"; icon: React.ReactNode; }; type Props = ToggleProps | NonToggleProps;
  3. After Propsでアイコンごとの責務を分離する 1 2 3 4 5 6 7 8

    9 export type BaseBouncyIconProps = { onClick: () => void; onAnimationEnd?: () => void; className?: string; }; export type PlayToPauseBouncyIconProps = BaseBouncyIconProps & { isPlaying: boolean; };
  4. After JSXの構造でUIの状態を表現する 1 2 3 4 5 6 7 8

    9 10 11 12 type ToggleProps = BaseProps & { // ... fromIcon: React.ReactNode; toIcon: React.ReactNode; }; return ( <BaseBouncyIconButton // ... fromIcon={<IconPlay className="size-full" />} toIcon={<IconPause className="size-full" />} />
  5. 「discriminated union」でUIの振る舞いを型に閉じ込める 「discriminated union」でUIの振る舞いを型に閉じ込める 1 2 3 4 5 6

    7 8 9 10 11 12 // 不可能な状態を許容してしまう type BadProgressBarProps = { status: 'loading' | 'success' | 'error'; percent?: number; // loadingの時だけほしい message?: string; // errorの時だけほしい }; // 判別可能なユニオン型 type GoodProgressBarProps = | { status: 'loading'; percent: number; } | { status: 'error'; message: string; } | { status: 'success'; };