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

TypeScript サーバーサイドエンジニアが関数型から学 ぶべき 3 つのアイディア

Avatar for majimaccho majimaccho
May 27, 2025
490

TypeScript サーバーサイドエンジニアが関数型から学 ぶべき 3 つのアイディア

Avatar for majimaccho

majimaccho

May 27, 2025
Tweet

Transcript

  1. 4

  2. 7

  3. Make illegal states unrepresentable 連絡先(ContactInfo)はメールアドレスか郵送先の どちらか、または両方を持つことができる どちらも持たない状態は許容されないというルールを型で表現する type EmailOnlyContactInfo =

    { type: "EmailOnly"; email: EmailContactInfo }; type PostalOnlyContactInfo = { type: "PostalOnly"; postal: PostalContactInfo }; type EmailAndPostalContactInfo = { type: "EmailAndPostal"; email: EmailContactInfo; postal: PostalContactInfo; }; type ContactInfo = | EmailOnlyContactInfo | PostalOnlyContactInfo | EmailAndPostalContactInfo; 16
  4. 振る舞いについても型で表現する メールを送信する関数は、メールアドレスが存在する場合にのみ呼び出せる ContactInfo の変更時にも型で影響の有無が明示される const sendEmail = async ( //

    PostalOnlyContactInfoは許容しない contact: EmailOnlyContactInfo | EmailAndPostalContactInfo ): Promise<EmailSendResult> => { // ここでEmailが存在することを型で保証されているので // 検証ロジックは不要 }; 18
  5. Parse, don’t validate (バリデーションするな、パースせよ) 外界からの信頼できない入力は可能な限り外側の層で信頼可能な値に Parse する Unvalidated(未検証)と Validated(検証済)明確に型レベルで区別する Always

    Valid Domain Model / セキュアバイデザインでも類似した考え方がある 不正な状態は存在してはいけないとすることで後続コードは全て入力値の正しさ を信じることができる 19
  6. Parse, don’t validate ではない例 const validateEmail = (emailStr: string): boolean

    => { // 無効なメールアドレスであれば false を返す // 有効なメールアドレスであれば true を返す }; const sendEmail = async (emailStr: string) => { if (!validateEmail(emailStr)) { throw new InvalidEmailAddressError(emailStr); } // このままでは EmailService.sendはemailStr が不正な値である可能性がある await EmailService.send(emailStr); }; 20
  7. Parse, don’t validate の例 const parseEmail = ( unvalidatedEmail: string

    ): | { isOk: true; value: ValidatedEmail } | { isOk: false; error: InvalidEmailFormatError } => { // 有効なメールアドレスであればtrueではなく // { isOk: true, value: ValidatedEmail } を返す // 無効なメールアドレスであれば // { isOk: false, error: InvalidEmailFormatError } を返す }; const sendEmail = async (unvalidatedEmail: string) => { const parseResult = parseEmail(unvalidatedEmail); if (!parseResult.isOk) { return parseResult.error; } // EmailService.sendは引数の値を信用できるため再度検証する必要はない await EmailService.send(parseResult.value); }; 21
  8. Result 型 TS の try-catch はエラーを unknown にする どの関数が throw

    するのかインターフェースから理解不能 23
  9. さっきの Parse, don’t validate の例は Result 型を返している const parseEmail =

    ( unvalidatedEmail: string ): | { isOk: true; value: ValidatedEmail } | { isOk: false; error: InvalidEmailFormatError } => {...}; const sendEmail = async (unvalidatedEmail: string) => { const parseResult = parseEmail(unvalidatedEmail); if (!parseResult.isOk) { return parseResult.error; } await EmailService.send(parseResult.value); }; 24
  10. 型定義から起こりうるエラーが明示されている const parseEmail = ( unvalidatedEmail: string ): | {

    isOk: true; value: ValidatedEmail } | { isOk: false; error: InvalidEmailFormatError } => { // 無効なメールアドレスであれば // { isOk: false, error: InvalidEmailFormatError } を返す // 有効なメールアドレスであれば // { isOk: true, value: ValidatedEmail } を返す }; 25
  11. 呼び出し時のエラーハンドリングも改善される 起こりうるエラーが明示的なのでエラー型ごとに個別に対処できる Result 型 const sendEmail = async (unvalidatedEmail: string)

    => { const parseResult = parseEmail(unvalidatedEmail); if (!parseResult.isOk) { // isOkがfalseの場合が起こりうるので処理が必要であることが明示的 return parseResult.error; } await EmailService.send(parseResult.value); }; 26
  12. 汎用 Result 型を定義する type Result<T, E> = | { isOk:

    true; value: T } | { isOk: false; error: E }; // Result 型を使って parseEmail を定義する const parseEmail = ( unvalidatedEmail: string ): Result<ValidatedEmail, InvalidEmailFormatError> => {...} 27
  13. 関数型のアイディアの段階的採用 Parse, don't validate から始める Parse, don't validate 自体は関数型コミュニティでのスローガンだが、コアとなる考 え方は

    Always Valid Domain Model / セキュアバイデザインに近い クラス指向的な設計であっても取り入れやすいし部分的にも適用可能 クラスを使わない表現や Result 型は統一感がないと混乱を招きやすい 31
  14. 32