Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Taiga Mikami
November 02, 2023
1.3k

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

Taiga Mikami

November 02, 2023
Tweet

Transcript

  1. ta1m1kam is 誰 📝 Shippioフロントエンドエンジニア 🎨 React書いてます なんでもやってます 🧑‍💻 ハッカソンたくさん出たり

    💪 筋トレ好きです 🏋️‍♀️ 好きな種目: スカルクラッシャー 🔤 英語精進中 で作成してみました。 Slidev
  2. 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
  3. 移行前に… 現行のサービスはおよそ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コンフリクトでマニュアルで頑張る
  4. ts-pattern 🎨 in Shippio 🚢 前提 Shippioではユーザタイプだけでなく、シップメントやコンテナ状態を管理するenum値が多い 元々FEでenum値等を定義していた 🙅‍♂️ Graphql

    code generator導入 BEで定義したGraphQLスキーマと同じ型を使いたい! FEのenum値を置き換え enum値によるロジックの分岐の際にもっといい感じにできないか? 🤔 ここでts-pattern登場!
  5. 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, } } ` `
  6. 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; } }
  7. 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)
  8. 複雑な条件分岐も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(); }
  9. 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');
  10. ワイルドカード 独自ロジックチェック ガードチェック 🚨数値のenumはexhaustive()使用できない… const input = 2; const output

    = match<number | string>(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 } }
  11. Shippio 🚢での使用例 ユーザーの種類によってレンダリングするコンポー ネントの出し分け 影響範囲がコンパイルエラーとして検知できる! シップメントやコンテナの種類等でもEnum値の頻繁 更新が起こり得る 複合パターンもReadableに対応できる! const currentUser

    = useAuth(); const renderComponent = match(currentUser.loginUserType) .with(UserType.Customer, () => <CustomerComponent >) .with(UserType.Warehouse, () => <WarehouseComponent >) .with(UserType.Forwarder, () => <ForwarderComponent >) .with(UserType.Subsidiary, () => <SubsidiaryComponent >) .with(UserType.ShippioStaff, () => <ShippioStaffComponent >) .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} </> )