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_から卒業する.pdf
Search
ディップ株式会社
PRO
June 18, 2026
24
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
_型ガードしたのにnullable_から卒業する.pdf
ディップ株式会社
PRO
June 18, 2026
More Decks by ディップ株式会社
See All by ディップ株式会社
はじめての環境構築!デプロイ〜Docker基礎を学べるワークショップ!
dip_tech
PRO
0
37
【TSKaigi2026登壇資料】決定論的な型チェックへ Go 製コンパイラによる10倍速の裏側で stableTypeOrdering から見える並列化への挑戦
dip_tech
PRO
2
390
【TSKaigi2026登壇資料】バイトル」のTypeScriptリニューアル — 積み上がったレガシーとパフォーマンスに挑む現在地
dip_tech
PRO
1
360
【新卒研修】ライブデモ + compose.yaml読解_講義資料
dip_tech
PRO
0
250
【ディップ|26年新卒研修資料】OpenAPI/Swagger REST API研修
dip_tech
PRO
0
390
【ディップ|26年新卒研修資料】Docker_ハンズオン研修
dip_tech
PRO
0
360
【ディップ|26年新卒研修資料】TDD実装演習
dip_tech
PRO
0
420
ハッカソンや個人開発で何作る? テーマ発見〜アイデア発想ハンズオン! 技育CAMPアカデミア
dip_tech
PRO
0
90
技育祭登壇|「AIを使える」は、勘違いだった。 コードが書けてもプロになれなかった僕の1年戦記
dip_tech
PRO
0
140
Featured
See All Featured
Build The Right Thing And Hit Your Dates
maggiecrowley
39
3.2k
Raft: Consensus for Rubyists
vanstee
141
7.5k
Fashionably flexible responsive web design (full day workshop)
malarkey
408
66k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
First, design no harm
axbom
PRO
2
1.2k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
2
1.1k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.3k
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
220
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.5k
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
2k
For a Future-Friendly Web
brad_frost
183
10k
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.1k
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で、不可能な状態を書けなくしよう
We are hiring ⼀緒に変⾰に挑戦する仲間を募集中です!