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

TSで型安全なエラーハンドリング〜まずはBranded Typeで始めてみては?〜

TSで型安全なエラーハンドリング〜まずはBranded Typeで始めてみては?〜

TypeScriptのtry catchではerrorがunknown型になってしまい、エラーの情報が失われてしまいます。
安全にエラーハンドリングするためにResult型やEither型などが知られていますが、まずは小さくBranded Typeから始めてみるのはどうでしょう?という提案です!

Transcript

  1. 1. try catch のerror は型安全じゃない catch で何が補足されるかは全くわからない… const invalidJson =

    '{"name": "John",}'; try { const data = JSON.parse(invalidJson); console.log(data) } catch (error) { // error is unknown 😱 console.error(error.message); // unknown にはアクセスできない } 4 / 25
  2. 2. 型から例外の可能性が読み取れない 事前に例外を予期することができない type User = {name: string, age: number};

    // この関数、例外投げるかどうか分からない... const parseUserData = (data: {name: string, age: number}): User => { // ... バリデーションの処理 const {name, age} = data return {name, age} } // 使う側は知らずに爆死 const = parseUserData({name: 'hoge', age: 100}); // 💥 user 5 / 25
  3. TS は構造的部分型 構造的部分型 part1 type UserId = string; type Email

    = string; const sendEmail = (email: Email) => { /* ... */ } const userId: UserId = "user123"; sendEmail(userId); 9 / 25
  4. TS は構造的部分型 型のエイリアス・名前に意味はない 部分的に同じ構造なら別の型でも受け入れる 構造的部分型 part2 type Person = {

    name: string; age: number } type User = { id: UserId; email: Email; name: string; age: number; } const doSomethingPerson(u: Person) => {/** */} const user: User = {id: "id", email: "[email protected]", name: "John", age: 30}; doSomethingPerson(user) // エラーにならない 10 / 25
  5. 公称型は? 型のエイリアス自体が一致しているかが重要 同じ構造だからって許さんぞ! type UserId string type Email string func

    sendEmail(email Email) { // メール送信処理 } uid := UserId("id") sendEmail(uid) // コンパイルエラー!✅ email := Email("[email protected]") sendEmail(email) // ちゃんとパスする 11 / 25
  6. Branded Type 型名を表す一意なタグをつけてあげる type UserId = string & { __brand:

    'UserId' }; type Email = string & { __brand: 'Email' }; type = { User __brand: "User" id: UserId; email: Email; } 13 / 25
  7. Branded Type で公称型みたいなことができる type UserId = string; type Email =

    string & { __brand: 'Email' }; const Email = (email: string): Email => Object.assign(email, { __brand: 'Email' as const}); const sendEmail = ( : Email) => { /* ... */ } email const userId: UserId = 'userId1234'; sendEmail(userId); // コンパイルエラー!✅ const email: Email = Email("[email protected]"); sendEmail(email); // ちゃんとパスする 14 / 25
  8. Branded Type で公称型みたいなことができる 普通に Branded Type type UserId = string;

    type Email = string; const sendEmail = ( : Email) => { /* ... */ } email const userId: UserId = "user123"; sendEmail(userId); const : Email = "[email protected]"; email sendEmail(userId); 15 / 25 type UserId = string; type Email = string & { __brand: 'Email' }; const Email = (email: string): Email => Object.assign(email, { __brand: 'Email' as const}); const sendEmail = ( : Email) => { /* ... */ } email const userId: UserId = 'userId1234'; sendEmail(userId); // コンパイルエラー!✅ const email: Email = Email("[email protected]"); sendEmail(email); // ちゃんとパスする 15 / 25
  9. Error 型を定義 エラーも値として扱う type UserNameValidationError = {__brand: 'UserNameValidationError', message: string}

    const UserNameValidationError = (message: string): UserNameValidationError => ({ __brand: 'UserNameValidationError', message, }); type UserAgeValidationError = {__brand: 'UserAgeValidationError', message: string} const UserAgeValidationError = (message: string): UserAgeValidationError => ({ __brand: 'UserAgeValidationError', message, }); 18 / 25
  10. 関数の戻り値でエラーの可能性を表現 一目でエラーの可能性がわかる 型安全 const parseUser = (name: string, age: number):

    User | UserNameValidationError | UserAgeValidationError => { if (name.length > 10) return UserNameValidationError('name must be less than 10 characters') if (age < 0) return UserAgeValidationError('age must be positive') return User(name, age) } const = () => { main const result = parseUser('John Doe', 30) if (result.__brand === 'UserNameValidationError') { toast.error(`UserName が不正です: ${result.message}`) return } else if (result.__brand === 'UserAgeValidationError') { toast.error(`UserAge が不正です: ${result.message}`) return } const user = result console.log(user.name, user.age); } 19 / 25
  11. まとめ 1. TS の例外処理(try catch )は型安全じゃない error はunknown 型から例外が読み取れない 2.

    Branded Type で解決 エラーも値として扱う 型を一意に特定するタグフィールド 型の絞り込みでエラーハンドリングを強制 23 / 25 23 / 25