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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
クラウドファンディング版StackChan 3体(4体)をインタラクティブな体験型作品にして展示もした話 / スタックチャンお誕生日会2026
you
PRO
0
140
【Cyber-sec+】経営層を"動かす"ための考え方
hssh2_bin
0
200
事業会社における 機械学習・推薦システム技術の活用事例と必要な能力 / ml-recsys-in-layerx-wantedly-2026
yuya4
0
110
コミットの「なぜ」を読む
ota1022
0
100
“詰む”前に仕組みを作れ 〜技術の波に溺れないためのキャッチアップ術〜
takasyou
6
2.7k
SONiCのLinuxベースを活かしたZabbix監視
sonic
0
260
あなたの知らないPDFのアクセシビリティ
lycorptech_jp
PRO
0
220
When Platform Engineering Meets GenAI
sucitw
0
150
螺旋型キャリアの生存戦略 / kinoko-conf2026
rakus_dev
1
570
【Snowflake Summit 2026 Recap!!】Snowflake Summit Deep Dive: Security & Governance
civitaspo
1
290
MUSUBI 田中裕一『AIと共に行う「しごとのリデザイン」- スモールバックオフィス編』AI Ops Lab #4
musubi
0
280
フィジカル版Github Onshapeの紹介
shiba_8ro
0
310
Featured
See All Featured
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
370
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
870
Google's AI Overviews - The New Search
badams
0
1k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
Embracing the Ebb and Flow
colly
88
5.1k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Building Flexible Design Systems
yeseniaperezcruz
330
40k
Art, The Web, and Tiny UX
lynnandtonic
304
22k
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.1k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
Into the Great Unknown - MozCon
thekraken
41
2.6k
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で、不可能な状態を書けなくしよう