Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
「型ガードしたのにnullable」から卒業する
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Hayato Yokoyama
June 18, 2026
Technology
150
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
110
AIが特別じゃなくなった時代に、作ることを楽しもう
hayato_yokoyama
0
38
AIのためのテスト戦略 〜TDDが難しいフロントエンド開発でのアプローチ〜
hayato_yokoyama
0
280
実はすごいスピードで進化しているCSS
hayato_yokoyama
0
260
Next.js AppRouter × GraphQL 〜 夢見た理想と現実の課題 〜
hayato_yokoyama
0
180
フロントエンドテストを書きやすくするために工夫したこと
hayato_yokoyama
1
95
Other Decks in Technology
See All in Technology
Codex概要
ymiya55
0
280
OpenSharing について熱く語る〜AI アセットの共有について〜
kameitomohiro
0
180
カンファレンスに参加した後の浮遊感とセルフケア
pauli
0
310
Claude Designがめちゃくちゃ便利なので使ってほしい
diggymo
0
280
自主式軟體工廠
philipz
0
150
[2026-09-30]ロックンロールは鳴り止まないっ - 信頼性かまってちゃん - 「データ駆動を投げ捨ててまで。」追いかける信頼性改善に向けた取り組みの話
tosite
0
190
IR Today: Theory, Practice, and Agents
dtunkelang
0
340
1人アドミンな私はAWSアカウント申請をSlackで完結したい!
ysuzuki
0
110
【データ横丁主催】AI Agentがコンテキストを使って仕事をした後、何が残るのか― 組織の経験を次の判断に引き継ぐ「Agent Memory」
shisyu_gaku
2
320
HolmesGPTで始めるSREエージェント入門!プラットフォームの障害調査はAIにお任せ 〜
leveragestech
PRO
0
140
おそらく日本で唯一のDevRelインターン生として
husengs7
0
160
20260929_AmazonGuardDutyの検出通知メールにAWS DevOpsAgentの調査結果を追加する
yhana
1
430
Featured
See All Featured
GraphQLの誤解/rethinking-graphql
sonatard
75
12k
Fashionably flexible responsive web design (full day workshop)
malarkey
409
67k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
560
First, design no harm
axbom
PRO
2
1.3k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
250
Fireside Chat
paigeccino
43
4k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.2k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
270
Paper Plane (Part 1)
katiecoart
PRO
2
11k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.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で、不可能な状態を書けなくしよう