Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
「型ガードしたのにnullable」から卒業する
Search
Hayato Yokoyama
June 18, 2026
Technology
100
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
「型ガードしたのにnullable」から卒業する
Hayato Yokoyama
June 18, 2026
More Decks by Hayato Yokoyama
See All by Hayato Yokoyama
「バイトル」のTypeScriptリニューアル — 積み上がったレガシーとパフォーマンスに挑む現在地
hayato_yokoyama
0
26
AIが特別じゃなくなった時代に、作ることを楽しもう
hayato_yokoyama
0
27
AIのためのテスト戦略 〜TDDが難しいフロントエンド開発でのアプローチ〜
hayato_yokoyama
0
250
実はすごいスピードで進化しているCSS
hayato_yokoyama
0
240
Next.js AppRouter × GraphQL 〜 夢見た理想と現実の課題 〜
hayato_yokoyama
0
170
フロントエンドテストを書きやすくするために工夫したこと
hayato_yokoyama
1
89
Other Decks in Technology
See All in Technology
【Snowflake Summit 2026 Recap!!】Snowflake Summit Deep Dive: Security & Governance
civitaspo
1
290
秘密度ラベル初心者が第1歩でつまづかないための「設計・運用」ポイント
seafay
PRO
1
410
気軽に使える"情報のハブ"としてのNotion活用 〜フロー情報の集積点 と、 Claude Code × Notion AI〜
syucream
1
160
「軸足」は 固定しなくていい - 熱量と強みで描く、しなやかなキャリアの形
kakehashi
PRO
1
180
ACE-Step-1.5で見る 音楽生成AIのしくみと“破綻だけ直す”Retake機能の開発【zennfes spring 2026 登壇資料】
personabb
1
550
AIのReact習熟度を測る
uhyo
2
660
Claude Codeをどのように キャッチアップしているか
oikon48
13
8.7k
【2026年版】 ベクトル検索とEmbedding最前線
mocobeta
23
6.7k
AI-DLCを “そのまま導入しなかった”話 ~組織に合わせてアジャストした 私たちの実践共有~
hiroramos4
PRO
1
340
螺旋型キャリアの生存戦略 / kinoko-conf2026
rakus_dev
1
570
SteampipeとExcel Power QueryでAWS構成定義書の作成を自動化する
jhashimoto
0
170
iOS アプリの「これって不具合ですか?」を AI に調べてもらう
miichan
0
120
Featured
See All Featured
From π to Pie charts
rasagy
0
220
4 Signs Your Business is Dying
shpigford
187
22k
Design in an AI World
tapps
1
250
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
740
Practical Orchestrator
shlominoach
191
11k
Designing for humans not robots
tammielis
254
26k
Speed Design
sergeychernyshev
33
1.9k
The B2B funnel & how to create a winning content strategy
katarinadahlin
PRO
1
390
Art, The Web, and Tiny UX
lynnandtonic
304
22k
The Illustrated Children's Guide to Kubernetes
chrisshort
51
52k
[SF Ruby Conf 2025] Rails X
palkan
2
1.1k
Making Projects Easy
brettharned
120
6.7k
Transcript
「型ガードしたのにnullable」から卒業する ディップ株式会社 横⼭ 隼 2026/06/18 アフターイベント TSKaigi 2026 しか型ん!
横⼭ 隼 Yokoyama Hayato • アルバイト求⼈サービス「バイトル」の エンジニア • TypeScript,React Routerで
バイトルのリニューアルしています • TSKaigiでは 弊社のスポンサーセッションをしました • dipの「d」をやったつもりが「b」だった
Copyright © DIP Corporation, All rights reserved. 早速ですが、 こんな型をよく⾒ない?? パート①
Copyright © DIP Corporation, All rights reserved. 読み込み中‧エラー‧データを表すシンプルなカードUI type JobCardProps
= { isLoading: boolean; error: Error | null; job: JobData | null; };
Copyright © DIP Corporation, All rights reserved. 読み込み中‧エラー‧データを表すシンプルなカードUI type JobCardProps
= { isLoading: boolean; error: Error | null; job: JobData | null; }; • ⾮同期でデータ取ってきて表⽰するコンポーネント全般でよくある • useQuery (TanStack Query) や useSWR の戻り値もこんな感じ → { isLoading, error, data } = useQuery(...);
Copyright © DIP Corporation, All rights reserved. これだと通常ありえない <JobCard>が書けてしまう
Copyright © DIP Corporation, All rights reserved. ロード中なのに、求⼈データがある <JobCard isLoading={true}
error={null} job={jobData} />
Copyright © DIP Corporation, All rights reserved. ロード中なのに、求⼈データがある <JobCard isLoading={true}
error={null} job={jobData} />
Copyright © DIP Corporation, All rights reserved. ロード中なのに、求⼈データがある <JobCard isLoading={true}
error={null} job={jobData} /> エラーなのに、求⼈データがある <JobCard isLoading={false} error={new Error()} job={jobData} />
Copyright © DIP Corporation, All rights reserved. ロード中なのに、求⼈データがある <JobCard isLoading={true}
error={null} job={jobData} /> エラーなのに、求⼈データがある <JobCard isLoading={false} error={new Error()} job={jobData} />
Copyright © DIP Corporation, All rights reserved. ロード中なのに、求⼈データがある <JobCard isLoading={true}
error={null} job={jobData} /> エラーなのに、求⼈データがある <JobCard isLoading={false} error={new Error()} job={jobData} /> TypeScriptはどちらも型エラーにしてくれない
Copyright © DIP Corporation, All rights reserved. こんな<JobCard>を書いてしまうとどうなるか?
Copyright © DIP Corporation, All rights reserved. function JobCard({ isLoading,
error, job }: JobCardProps) { if (isLoading) return <Spinner />; if (error) return <ErrorMessage err={error} />; return ( <div> <h2>{job?.title ?? ""}</h2> <p>{job!.companyName}</p> </div> ); } こんな<JobCard>を書いてしまうとどうなるか?
Copyright © DIP Corporation, All rights reserved. function JobCard({ isLoading,
error, job }: JobCardProps) { if (isLoading) return <Spinner />; if (error) return <ErrorMessage err={error} />; return ( <div> <h2>{job?.title ?? ""}</h2> <p>{job!.companyName}</p> </div> ); } こんな<JobCard>を書いてしまうとどうなるか? isLoading: falseもerror: nullも 確認したのに
Copyright © DIP Corporation, All rights reserved. function JobCard({ isLoading,
error, job }: JobCardProps) { if (isLoading) return <Spinner />; if (error) return <ErrorMessage err={error} />; return ( <div> <h2>{job?.title ?? ""}</h2> <p>{job!.companyName}</p> </div> ); } こんな<JobCard>を書いてしまうとどうなるか? isLoading: falseもerror: nullも 確認したのに TypeScriptは jobがまだnullableだと思ってる
Copyright © DIP Corporation, All rights reserved. function JobCard({ isLoading,
error, job }: JobCardProps) { if (isLoading) return <Spinner />; if (error) return <ErrorMessage err={error} />; return ( <div> <h2>{job?.title ?? ""}</h2> <p>{job!.companyName}</p> </div> ); } こんな<JobCard>を書いてしまうとどうなるか? isLoading: falseもerror: nullも 確認したのに TypeScriptは jobがまだnullableだと思ってる ➡ 型ガードしたはずなのに、jobには効かず ?? や ! が散らばる
Copyright © DIP Corporation, All rights reserved. こんな型をよく⾒ない?? パート②
Copyright © DIP Corporation, All rights reserved. みんな⼤好き、Buttonコンポーネント type ButtonProps
= { href: string | null; onClick: (() => void) | null; children: ReactNode; }; 同じボタンの⾒た⽬で 「ボタン」として動作するときと「リンク」として動作するときがある
Copyright © DIP Corporation, All rights reserved. これもありえない<Button>が書けてしまう hrefもあるし、onClickもある (※仕様によってはこれが正しい場合もあるけど)
<Button href="/about" onClick={() => alert("hoge!")}>クリック</Button>
Copyright © DIP Corporation, All rights reserved. これもありえない<Button>が書けてしまう hrefもあるし、onClickもある (※仕様によってはこれが正しい場合もあるけど)
<Button href="/about" onClick={() => alert("hoge!")}>クリック</Button>
Copyright © DIP Corporation, All rights reserved. これもありえない<Button>が書けてしまう hrefもあるし、onClickもある (※仕様によってはこれが正しい場合もあるけど)
<Button href="/about" onClick={() => alert("hoge!")}>クリック</Button> 画面遷移もしないし、クリックイベントも発生しない <Button href={null} onClick={null}>クリック</Button>
Copyright © DIP Corporation, All rights reserved. これもありえない<Button>が書けてしまう hrefもあるし、onClickもある (※仕様によってはこれが正しい場合もあるけど)
<Button href="/about" onClick={() => alert("hoge!")}>クリック</Button> 画面遷移もしないし、クリックイベントも発生しない <Button href={null} onClick={null}>クリック</Button>
Copyright © DIP Corporation, All rights reserved. こんな<Button>を書いてしまうとどうなるか?
Copyright © DIP Corporation, All rights reserved. こんな<Button>を書いてしまうとどうなるか? function Button({
href, onClick, children }: ButtonProps) { if (!href && !onClick) return null; if (href && onClick) return ...; if (href) return <a href={href}>{children}</a>; return <button onClick={onClick!}>{children}</button>; }
Copyright © DIP Corporation, All rights reserved. こんな<Button>を書いてしまうとどうなるか? function Button({
href, onClick, children }: ButtonProps) { if (!href && !onClick) return null; if (href && onClick) return ...; if (href) return <a href={href}>{children}</a>; return <button onClick={onClick!}>{children}</button>; } リンクもクリックイベントもない → 何もしないボタン?
Copyright © DIP Corporation, All rights reserved. こんな<Button>を書いてしまうとどうなるか? function Button({
href, onClick, children }: ButtonProps) { if (!href && !onClick) return null; if (href && onClick) return ...; if (href) return <a href={href}>{children}</a>; return <button onClick={onClick!}>{children}</button>; } リンクもクリックイベントもない → 何もしないボタン? リンクもクリックイベントも両⽅ある → どっちを優先する?
Copyright © DIP Corporation, All rights reserved. こんな<Button>を書いてしまうとどうなるか? ➡ 仕様ではなく型の⽳埋めのためのif⽂が⽣まれる
function Button({ href, onClick, children }: ButtonProps) { if (!href && !onClick) return null; if (href && onClick) return ...; if (href) return <a href={href}>{children}</a>; return <button onClick={onClick!}>{children}</button>; } リンクもクリックイベントもない → 何もしないボタン? リンクもクリックイベントも両⽅ある → どっちを優先する?
Copyright © DIP Corporation, All rights reserved. JobCardとButton、根本原因は同じ • 実際に存在し得る状態以上に型が表現出来てしまっている
• JobCardPropsは boolean × (Error | null) × (JobData | null) = 2×2×2 = 8通り書けていた • でも実際にありえるのは loading / error / success の3通りだけ
Copyright © DIP Corporation, All rights reserved. Make Impossible States
Impossible 「不可能な状態が起こらないようにインターフェイスを設計しよう」という設計思想
Copyright © DIP Corporation, All rights reserved. どうやったらありえない型を「書けなく」できる?
Copyright © DIP Corporation, All rights reserved. 解法① Discriminated Union
type JobCardProps = | { status: "loading" } | { status: "error"; error: Error } | { status: "success"; job: JobData }; 1. 状態ごとに型を分ける (ここではloading, error, success) 2. 判別するための共通フィールドを持たせる (ここではstatus) ※ サバイバルTypeScriptがとても参考になります
Copyright © DIP Corporation, All rights reserved. ⽐較してみると... ⾮Discriminated Union
type JobCardProps = { isLoading: boolean; error: Error | null; job: JobData | null; }; Discriminated Union type JobCardProps = | { status: "loading" } | { status: "error"; error: Error } | { status: "success"; job: JobData }; 8通り(うち5通りはありえない) 3通り(ありえる状態と⼀致)
Copyright © DIP Corporation, All rights reserved. Discriminated Union化すると、ありえない <JobCard>
が書けなくなる <JobCard status="loading" job={jobData} /> // ^^^^^^^^^^^^ // Error: Property 'job' does not exist on type '{ status: "loading" }' ロード中なのに、求⼈データを渡そうとすると... エラーなのに、求⼈データを渡そうとすると... <JobCard status="error" error={err} job={jobData} /> // ^^^^^^^^^^^^ // Error: Property 'job' does not exist on type '{ status: "error"; error: Error }'
Copyright © DIP Corporation, All rights reserved. Discriminated Union化すると、ありえない <JobCard>
が書けなくなる <JobCard status="loading" job={jobData} /> // ^^^^^^^^^^^^ // Error: Property 'job' does not exist on type '{ status: "loading" }' ロード中なのに、求⼈データを渡そうとすると... エラーなのに、求⼈データを渡そうとすると... <JobCard status="error" error={err} job={jobData} /> // ^^^^^^^^^^^^ // Error: Property 'job' does not exist on type '{ status: "error"; error: Error }' ➡ TypeScript が「ありえない型」を全部弾いてくれる
Copyright © DIP Corporation, All rights reserved. <JobCard>から ! や
?? が消える function JobCard(props: JobCardProps) { if (props.status === "loading") return <Spinner />; if (props.status === "error") return <Error err={props.error} />; // ここで props.status === "success" が確定 return ( <div> <h2>{props.job.title}</h2> <p>{props.job.companyName}</p> <p>時給 {props.job.hourlyWage.toLocaleString()}円</p> </div> ); }
Copyright © DIP Corporation, All rights reserved. <JobCard>から ! や
?? が消える function JobCard(props: JobCardProps) { if (props.status === "loading") return <Spinner />; if (props.status === "error") return <Error err={props.error} />; // ここで props.status === "success" が確定 return ( <div> <h2>{props.job.title}</h2> <p>{props.job.companyName}</p> <p>時給 {props.job.hourlyWage.toLocaleString()}円</p> </div> ); } Jobが確実に存在するので、 ! や ?? が消える
Copyright © DIP Corporation, All rights reserved. 他にもバイトルでの使われ⽅ • 同じパスで「掲載中」と「掲載終了」を表現する仕事詳細ページ
• React Routerのloaderで求⼈情報を取得する処理 export async function loader({ params }) { const result = await fetchJobDetail(jobId); if (result.kind === "expired") { return data({ kind: "expired" as const, ... }, { status: 404 }); } return data({ kind: "active" as const, jobId, jobTitle, ... }); }
Copyright © DIP Corporation, All rights reserved. 他にもバイトルでの使われ⽅ • 同じパスで「掲載中」と「掲載終了」を表現する仕事詳細ページ
• React Routerのloaderで求⼈情報を取得する処理 export async function loader({ params }) { const result = await fetchJobDetail(jobId); if (result.kind === "expired") { return data({ kind: "expired" as const, ... }, { status: 404 }); } return data({ kind: "active" as const, jobId, jobTitle, ... }); } as const で判別子をリテラル型に固めて返す
Copyright © DIP Corporation, All rights reserved. 解法② オプショナルnever型の排他的論理和(XOR) •
どちらか⽚⽅しか持てない を型で表現 • 持たない⽅のフィールドに ?: never を付ける • Discriminated Unionのような判別⼦は不要 type ButtonProps = | { href: string; onClick?: never; children: ReactNode } | { href?: never; onClick: () => void; children: ReactNode };
Copyright © DIP Corporation, All rights reserved. ⽐較してみると... ⾮XOR XOR
(?: never) type ButtonProps = { href: string | null; onClick: (() => void) | null; children: ReactNode; }; type ButtonProps = | { href: string; onClick?: never; children: ReactNode } | { href?: never; onClick: () => void; children: ReactNode };
Copyright © DIP Corporation, All rights reserved. XOR化すると、ありえない <Button> が書けなくなる
href と onClick を両⽅渡そうとすると... href も onClick もない場合 <Button href="/about" onClick={() => alert("!")}>クリック</Button> // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Error: Type '() => void' is not assignable to type 'undefined' <Button>クリック</Button> // Error: Property 'href' or 'onClick' is required
Copyright © DIP Corporation, All rights reserved. XOR化すると、ありえない <Button> が書けなくなる
href と onClick を両⽅渡そうとすると... href も onClick もない場合 ➡ TypeScript が「ありえない型」を全部弾いてくれる <Button href="/about" onClick={() => alert("!")}>クリック</Button> // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Error: Type '() => void' is not assignable to type 'undefined' <Button>クリック</Button> // Error: Property 'href' or 'onClick' is required
Copyright © DIP Corporation, All rights reserved. まとめ • Make
Impossible States Impossible で不可能な状態が起こらないように インターフェイスを設計しよう • ! や ?? が増えてきたら、⾒直しの予兆 • Discriminated UnionやXORで、不可能な状態を書けなくしよう