見出し画像

ts-patternとneverthrowを使ったワークアラウンド~カスタムエラークラスを添えて~



まえがき

こちらの記事に遭遇したので、neverthrowのResult型を使ったバージョンを書いてみました。


デモ


https://codesandbox.io/p/sandbox/recursing-dhawan-xrdtc9?file=%2Fsrc%2Findex.ts%3A1%2C1


ramdaに関してはrambdaを使ってます


定義側になります

import { Result, err, ok } from "neverthrow";
import { pipe } from "rambda"
import { P, match } from "ts-pattern";

export abstract class ApplicationError extends Error {
  abstract status: number;
  abstract code: "A100" | "B200" | "C300" | "D400" | "E500";
  cause: unknown;

  constructor(message: string, options?: { cause: unknown }) {
    super(message);
    this.cause = options?.cause;
  }

  toJSON() {
    return {
      status: this.status,
      code: this.code,
      name: this.name,
      message: this.message,
      cause: this.cause,
      stack: this.stack,
    };
  }
}

export class ValidationError extends ApplicationError {
  override readonly code = "A100";
  override readonly name = "ValidationError" as const;
  status = 422 as const;
}
export const isValidationError = (error: unknown): error is ValidationError =>
  error instanceof ValidationError;

export class ImportError extends ApplicationError {
  override readonly code = "C300";
  override readonly name = "ImportError" as const;
  status = 422 as const;
}
export const isImportError = (error: unknown): error is ImportError =>
  error instanceof ImportError;

export class ExportError extends ApplicationError {
  override readonly code = "E500";
  override readonly name = "ExportError" as const;
  status = 422 as const;
}
export const isExportError = (error: unknown): error is ExportError =>
  error instanceof ExportError;

type InputF1 = unknown;
type OutputF1 = "func1's success output";
type InputF2 = OutputF1;
type OutputF2 = "func2's success output";
type InputF3 = OutputF2;
type OutputF3 = "func3's success output";

interface SampleFactory {
  f1(payload: InputF1): Result<OutputF1, ValidationError>
  f2(payload: InputF2): Result<OutputF2, ImportError>
  f3(payload: InputF3): Result<OutputF3, ExportError>
}

class SampleRepository implements SampleFactory {
  f1(payload: InputF1): Result<OutputF1, ValidationError> {
    return ok("func1's success output");
  }
  f2(payload: InputF2): Result<OutputF2, ImportError> {
    return ok("func2's success output");
  }
  f3(payload: InputF3): Result<OutputF3, ExportError> {
    return ok("func3's success output");
  }
}

const factory = {
  createRepository: () => new SampleRepository()
}

function bypass<
  PreviousOk,
  PreviousNg extends ApplicationError,
  NextOk,
  NextNg extends ApplicationError,
>(
  func: (latestInput: PreviousOk) => Result<NextOk, NextNg>
): (
  input: Result<PreviousOk, PreviousNg>
) => Result<NextOk, PreviousNg | NextNg> {
  return (input) => {
    if (input.isOk()) {
      return func(input.value);
    } else {
      return err(input.error);
    }
  };
}

const sampleRepository = factory.createRepository()

export function main(defaultValue: unknown) {
  return pipe(sampleRepository.f1, bypass(sampleRepository.f2), bypass(sampleRepository.f3), (result) => {
    if (result.isErr())
      return match(result.error)
        .with(P.when(isValidationError), (error) => error.message)
        .with(P.when(isImportError), (error) => error.message)
        .with(P.when(isExportError), (error) => error.message)
        .exhaustive();

    return result.value;
  })(defaultValue);
}


使用側になります

import { describe, test, expect } from 'vitest'
import { main } from '.'

describe("main", () => {
  test("passthrough", () => {
    const inputData = 123
    const outputData = main(inputData)
    expect(outputData).toStrictEqual("func3's success output")
  })
})


おわりに

カスタムエラークラスに関しては以下の記事が最近だと良さそうと思いました。


話はそれますが、ドメインモデルの側面でこちらの記事も良さそうと思いました。



こちらのコメントも合わせると、なんだかバックエンドできそうな気がしてきました。




前回の記事でも紹介しております。


まとめ

I/Oを徹底的に定義することがフロントエンドでもバックエンドでも大事そうですね。簡単ですが、以上です。


この記事が気に入ったらサポートをしてみませんか?