Slide 1

Slide 1 text

ts-pattern 🎨でより型安全な パターンマッチングを!! ta1m1kam

Slide 2

Slide 2 text

ta1m1kam is 誰 📝 Shippioフロントエンドエンジニア 🎨 React書いてます なんでもやってます 🧑‍💻 ハッカソンたくさん出たり 💪 筋トレ好きです 🏋️‍♀️ 好きな種目: スカルクラッシャー 🔤 英語精進中 で作成してみました。 Slidev

Slide 3

Slide 3 text

株式会社Shippio 会社紹介資料 20230901 株式会社Shippio 会社紹介資料_20230901 株式会社Shippio 会社紹介資料_20230901 by by by Shippio Shippio Shippio

Slide 4

Slide 4 text

Shippio フロントエンドの現状 顧客向けアプリ Shippioオペレーター向けアプリ 課題点 顧客向けアプリの方が機能が充実しがち オーバーラップしている機能もある 2重の開発をすることも… 2つのアプリケーションを運用

Slide 5

Slide 5 text

Shippio フロントエンドの現状 課題点 顧客向けアプリの方が機能が充実しがち オーバーラップしている機能もある 2重の開発をすることも… そうだ! monorepoにしよう! pnpm workspace + (turborepo) 共通のUIコンポーネント 共通の独自ビジネスロジック config設定を共通化 将来的に独自packageをOSSにすることも視野 . ├── apps │ ├── back-office // 管理画面 │ ├── front-office // 顧客向け │ └── storybook ├── packages │ ├── eslint-config-custom │ ├── test-utils │ ├── tsconfig │ ├── ui │ ├── shippio-utils │ └── ...etc ├── package.json ├── pnpm-workspace.yaml └── turbo.json

Slide 6

Slide 6 text

移行前に… 現行のサービスはおよそ3年前に作られていて技術負債がたくさん JavaScript TypeScript化 GraphQL Code Generator でスキーマの型生成 ts-pattern導入 ← ⭐今日のメインの話 React v16 Node.js v14 React v18 (in progress) Node.js v18 までバージョンアップ オレオレ実装 アプリケーションストラクチャを文章化 bullet-proofを元に作成 他packageのupdate Renovate botで自動化 最初はdependecyコンフリクトでマニュアルで頑張る

Slide 7

Slide 7 text

ts-pattern 🎨 in Shippio 🚢 前提 Shippioではユーザタイプだけでなく、シップメントやコンテナ状態を管理するenum値が多い 元々FEでenum値等を定義していた 🙅‍♂️ Graphql code generator導入 BEで定義したGraphQLスキーマと同じ型を使いたい! FEのenum値を置き換え enum値によるロジックの分岐の際にもっといい感じにできないか? 🤔 ここでts-pattern登場!

Slide 8

Slide 8 text

ts-pattern 🎨 とは? 複雑な条件比較を簡素化 タイプセーフなパターンマッチングを提供 TypeScriptにbuilt-inでないの? TC39で議論 ステージ1 将来的にはJavaScriptにbuilt-inとして提供され るかも...? (数年はかかりそう) proposal-pattern-matching Pattern matching syntax for ECMAScript HTML 5.1k 92

Slide 9

Slide 9 text

TypeScriptのパターンマッチングの問題点 TypeScript パターンの網羅なし 👎 Rust パターンの網羅チェックあり 👍 TypeScriptの場合: 新しい寿司 秋刀魚 Saury がスキーマに追加された場合コンパイルエラーにならない 😢 type Sushi = "Tuna" | "Salmon" | "Flatfish" | "Sardine"; const getPriceOfSushi = (sushi: Sushi): number => { switch (sushi) { case "Tuna": return 200; case "Salmon": return 150; case "Flatfish": return 300; case "Sardine": return 100; default: throw new Error("Unknown sushi type"); } } enum Sushi { Tuna, Salmon, Flatfish, Sardine, } fn get_price_of_sushi(sushi: Sushi) -> u8 { match sushi { Sushi::Tuna => 200, Sushi::Salmon => 150, Sushi::Flatfish => 300, Sushi::Sardine => 100, } } ` `

Slide 10

Slide 10 text

TypeScriptでも網羅チェックはできるが… never型 TypeScript Exhaustiveness checking eslint typescript-eslint :sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript TypeScript 14.0k 2.6k type Sushi = "Tuna" | "Salmon" | "Flatfish" | "Sardine"; const getPriceOfSushi = (sushi: Sushi): number => { switch (sushi) { case "Tuna": return 200; case "Salmon": return 150; case "Flatfish": return 300; case "Sardine": return 100; default: const _exhaustiveCheck: never = sushi return _exhaustiveCheck; } }

Slide 11

Slide 11 text

ts-patternを使うと… .exhaustive() 網羅的なチェック 👍 新しい寿司 秋刀魚 Saury がスキーマに追加された場合コンパイルエラーにして検知できる! .otherwise() デフォルト値を返すハンドラーもある const getPriceOfSushi = (sushi: Sushi): number => match(sushi) .with("Tuna", () => 200) .with("Salmon", () => 150) .with("Flatfish", () => 300) .with("Sardine", () => 100) .exhaustive() ` ` .otherwise(() => 0)

Slide 12

Slide 12 text

複雑な条件分岐もReadableに書ける - ts-pattern 処理を追加する場合を想像 💭 ts-patternで書かれている方が改修しやすいはず…! const price = (order: Order): number => { const [drink, size] = order; switch (drink) { case "coffee": switch (size) { case "small": return 100; case "medium": return 200; case "large": return 300; } case "tea": switch (size) { case "small": return 150; case "medium": return 250; case "large": return 350; } } } type Drink = "coffee" | "tea" | "soda"; type Size = "small" | "medium" | "large"; type Order = [Drink, Size]; const price = (order: Order): number => { return match(order) .with(["coffee", "small"], () => 100) .with(["coffee", "medium"], () => 200) .with(["coffee", "large"], () => 300) .with(["tea", "small"], () => 150) .with(["tea", "medium"], () => 250) .with(["tea", "large"], () => 350) .exhaustive(); }

Slide 13

Slide 13 text

Objectの比較 ChatGPT-4 ts-pattern使う場合 type Input = { type: 'user'; name: string } | { type: 'image'; src: string } | { type: 'video'; seconds: number }; function processInput(input: InputType): string { if (input.type === 'image') { return 'image'; } else if (input.type === 'video') { if (input.seconds === 10) { return 'video of 10 seconds.'; } } else if (input.type === 'user') { if (input.name) { return `user of name: ${input.name}`; } } return 'something else'; } const output = match(input) .with({ type: 'image' }, () => 'image') .with({ type: 'video', seconds: 10 }, () => 'video of 10 .with({ type: 'user' }, ({ name }) => `user of name: ${n .otherwise(() => 'something else');

Slide 14

Slide 14 text

ワイルドカード 独自ロジックチェック ガードチェック 🚨数値のenumはexhaustive()使用できない… const input = 2; const output = match(input) .with(P.string, () => 'it is a string!') .with(P.number, () => 'it is a number!') .with(P.boolean, () => 'it is a boolean!') .with(P.any, () => 'it will always match') .exhaustive(); console.log(output); // => 'it is a number!' const output = match({ score: 10 }) .with( { score: P.when((score): score is 5 => score === 5),} (input) => ' 😐' // input is inferred as { score: 5 } ) .with({ score: P.when((score) => score < 5) }, () => ' 😞 .with({ score: P.when((score) => score > 5) }, () => ' 🙂 .run(); console.log(output); // => ' 🙂' const blogPostPattern = { type: 'blogpost', title: P.string, description: P.string } as const; if (isMatching(blogPostPattern, value)) { // value: { type: 'blogpost', title: string, description: string } }

Slide 15

Slide 15 text

Shippio 🚢での使用例 ユーザーの種類によってレンダリングするコンポー ネントの出し分け 影響範囲がコンパイルエラーとして検知できる! シップメントやコンテナの種類等でもEnum値の頻繁 更新が起こり得る 複合パターンもReadableに対応できる! const currentUser = useAuth(); const renderComponent = match(currentUser.loginUserType) .with(UserType.Customer, () => ) .with(UserType.Warehouse, () => ) .with(UserType.Forwarder, () => ) .with(UserType.Subsidiary, () => ) .with(UserType.ShippioStaff, () => ) .exhaustive() return ( <> {renderComponent} > ) const renderComponent = match({ status, shipmentType }) .with({ status: StatusEum.InProgress, shipmentType: ShipmentType.shippio }, () => 特定の処理) .with({ status: StatusEnum.Completed, shipmentType: ShipmentType.shippio }, () => 特定の処理) .with({ status: P.union(StatusEnum.Critical, StatusEnum.Warning), shipmentType: P.any }, () => 特定の処理) .otherwise(() => null) return ( <> {renderComponent} > )

Slide 16

Slide 16 text

Thank you References github.com/gvergnaud/ts-pattern TypeScript Exhaustiveness checking switch-exhaustiveness-check ts-patternでTypeScriptにパターンマッチングを持ち込み、より型安全な世界へ Embracing ts-pattern My info github.com/ta1m1kam x.com/ta1m1kam