0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

effect-tsでmapErrorTagsのようなもの

Last updated at Posted at 2025-02-27

エラーをmapErrorで組み替えたりするが、それのやり方の比較確認のためのメモ
改善案は自動で推論してくれないので、微妙な気がしてき

前回

共通部

カスタムエラークラスやログ処理の共通クラス

class SleepNGBeforeError {
  static tagName = 'Sleep.Ng.Before'
  readonly _tag = SleepNGBeforeError.tagName

  static isClass(test: any): test is SleepNGBeforeError {
    return test._tag === SleepNGBeforeError.tagName
  }

  getMessage(site: string) {
    return `${site} - ng - Sleep NG before wait`
  }
}

class SleepNGAfterError {
  static tagName = 'Sleep.Ng.After'
  readonly _tag = SleepNGAfterError.tagName

  wait: number
  constructor(w: number) {
    this.wait = w
  }

  static isClass(test: any): test is SleepNGAfterError {
    return test._tag === SleepNGAfterError.tagName
  }

  getMessage(site: string) {
    return `${site} - ng - ${this.wait} - Sleep NG after wait`
  }
}

class OkData {
  wait: number
  constructor(w: number) {
    this.wait = w
  }

  static isClass(test: any): test is OkData {
    return test instanceof OkData
  }

  static getLastIndex(site: string) {
    const index = site.slice(-1)
    return Number(index)
  }

  getMessage(site: string) {
    return `${site} - ok - ${this.wait}`
  }
}

const checkLog = (text: string) =>
  Effect.sync(() => console.log(`time : ${new Date().toISOString()} - text: ${text}`))

冗長に感じるコード

いかのコードが長く感じる、err.errorが_tagを持っているかだけの判断をしたいだけだったが、
型情報を持たせたりする必要があるかどうかわからず、こうなってしまった。

function old_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = Effect.mapError(effectAndThen, (err) => {
    if (SleepNGBeforeError.isClass(err.error)) {
      return err.error
    } else if (SleepNGAfterError.isClass(err.error)) {
      return err.error
    } else {
      Effect.runPromise(checkLog(`mapError_sleepNg : UnknownException : ${err}`))
      return err
    }
  })
  return effectAll
}

上記の場合の型情報は以下

function old_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>): Effect.Effect<T, UnknownException | SleepNGBeforeError | SleepNGAfterError, never>

UnknownException | SleepNGBeforeError | SleepNGAfterError がエラーとして取得できる

改善案1 型情報を捨てる場合

型情報をすてて、単純にerr.errorを巻き上げたいだけの場合

function liftMapTags<A, R>(self: Effect.Effect<A, UnknownException, R>) {
  function hasTag(err: unknown) {
    return Predicate.hasProperty(err, '_tag') && Predicate.isString(err['_tag'])
  }

  return Effect.mapError(self, (error) => {
    if (hasTag(error.error)) {
      return error.error
    }

    Effect.runPromise(checkLog(`liftMapTags : UnknownException : ${error}`))
    return error
  })
}

function new1_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = liftMapTags(effectAndThen)
  return effectAll
}

上記の場合の型情報は以下

function new1_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>): Effect.Effect<T, unknown, never>

unknown がエラーとして取得できる

改善案2 型情報を捨てない場合

function liftMapTagsWithType<A, R, ErrorType extends { tagName: string }>(
  self: Effect.Effect<A, UnknownException, R>,
  tags: ErrorType[]
) {
  function isClass<ErrorType extends { tagName: string }>(
    tag: ErrorType,
    err: unknown
  ): err is ErrorType {
    return (
      Predicate.hasProperty(err, '_tag') &&
      Predicate.isString(err['_tag']) &&
      tag.tagName === err['_tag']
    )
  }

  return Effect.mapError(self, (error) => {
    for (const tag of tags) {
      if (isClass(tag, error.error)) {
        return error.error
      }
    }

    Effect.runPromise(checkLog(`liftMapTagsWithType : UnknownException : ${error}`))
    return error
  })
}

function new2_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = liftMapTagsWithType(effectAndThen, [SleepNGBeforeError, SleepNGAfterError])
  return effectAll
}

上記の場合の型情報は以下

function new2_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>): Effect.Effect<T, UnknownException | typeof SleepNGBeforeError | typeof SleepNGAfterError, never>

UnknownException | typeof SleepNGBeforeError | typeof SleepNGAfterError がエラーとして取得できる

実行確認

import { describe, test } from 'vitest'
import { Effect, Predicate } from 'effect'
import { UnknownException } from 'effect/Cause'

class SleepNGBeforeError {
  static tagName = 'Sleep.Ng.Before'
  readonly _tag = SleepNGBeforeError.tagName

  static isClass(test: any): test is SleepNGBeforeError {
    return test._tag === SleepNGBeforeError.tagName
  }

  getMessage(site: string) {
    return `${site} - ng - Sleep NG before wait`
  }
}

class SleepNGAfterError {
  static tagName = 'Sleep.Ng.After'
  readonly _tag = SleepNGAfterError.tagName

  wait: number
  constructor(w: number) {
    this.wait = w
  }

  static isClass(test: any): test is SleepNGAfterError {
    return test._tag === SleepNGAfterError.tagName
  }

  getMessage(site: string) {
    return `${site} - ng - ${this.wait} - Sleep NG after wait`
  }
}

class OkData {
  wait: number
  constructor(w: number) {
    this.wait = w
  }

  static isClass(test: any): test is OkData {
    return test instanceof OkData
  }

  static getLastIndex(site: string) {
    const index = site.slice(-1)
    return Number(index)
  }

  getMessage(site: string) {
    return `${site} - ok - ${this.wait}`
  }
}

const checkLog = (text: string) =>
  Effect.sync(() => console.log(`time : ${new Date().toISOString()} - text: ${text}`))

const testOptions = {
  timeout: 1000 * 100
}

function old_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = Effect.mapError(effectAndThen, (err) => {
    if (SleepNGBeforeError.isClass(err.error)) {
      return err.error
    } else if (SleepNGAfterError.isClass(err.error)) {
      return err.error
    } else {
      Effect.runPromise(checkLog(`mapError_sleepNg : UnknownException : ${err}`))
      return err
    }
  })
  return effectAll
}

function new1_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = liftMapTags(effectAndThen)
  return effectAll
}

function new2_mapError_sleepNg<T>(effectAndThen: Effect.Effect<T, UnknownException, never>) {
  const effectAll = liftMapTagsWithType(effectAndThen, [SleepNGBeforeError, SleepNGAfterError])
  return effectAll
}

function liftMapTagsWithType<A, R, ErrorType extends { tagName: string }>(
  self: Effect.Effect<A, UnknownException, R>,
  tags: ErrorType[]
) {
  function isClass<ErrorType extends { tagName: string }>(
    tag: ErrorType,
    err: unknown
  ): err is ErrorType {
    return (
      Predicate.hasProperty(err, '_tag') &&
      Predicate.isString(err['_tag']) &&
      tag.tagName === err['_tag']
    )
  }

  return Effect.mapError(self, (error) => {
    for (const tag of tags) {
      if (isClass(tag, error.error)) {
        return error.error
      }
    }

    Effect.runPromise(checkLog(`liftMapTagsWithType : UnknownException : ${error}`))
    return error
  })
}

function liftMapTags<A, R>(self: Effect.Effect<A, UnknownException, R>) {
  function hasTag(err: unknown) {
    return Predicate.hasProperty(err, '_tag') && Predicate.isString(err['_tag'])
  }

  return Effect.mapError(self, (error) => {
    if (hasTag(error.error)) {
      return error.error
    }

    Effect.runPromise(checkLog(`liftMapTags : UnknownException : ${error}`))
    return error
  })
}

describe('effect-ts Error対応の簡素化', () => {
  function oldNew<A>(sleepEffect: Effect.Effect<A, UnknownException, never>) {
    const oldEffectAll = old_mapError_sleepNg(sleepEffect)
    const old = Effect.runSyncExit(oldEffectAll)
    console.log(`old - ${old}`)

    const new1EffectAll = new1_mapError_sleepNg(sleepEffect)
    const _new1 = Effect.runSyncExit(new1EffectAll)
    console.log(`new:1 - ${_new1}`)

    const new2EffectAll = new2_mapError_sleepNg(sleepEffect)
    const _new2 = Effect.runSyncExit(new2EffectAll)
    console.log(`new:2 - ${_new2}`)
  }

  test('使い方1 - succeed', testOptions, async () => {
    const sleepEffect = Effect.succeed(new OkData(123))
    oldNew(sleepEffect)
  })
  test('使い方2 - fail - SleepNGBeforeError', testOptions, async () => {
    const sleepEffect = Effect.fail(new UnknownException(new SleepNGBeforeError()))
    oldNew(sleepEffect)
  })
  test('使い方3 - fail - SleepNGAfterError', testOptions, async () => {
    const sleepEffect = Effect.fail(new UnknownException(new SleepNGAfterError(123)))
    oldNew(sleepEffect)
  })
  test('使い方4 - fail - UnknownException', testOptions, async () => {
    const sleepEffect = Effect.fail(new UnknownException(new Error()))
    oldNew(sleepEffect)
  })
})

使い方1

old - {
  "_id": "Exit",
  "_tag": "Success",
  "value": {
    "wait": 123
  }
}
new:1 - {
  "_id": "Exit",
  "_tag": "Success",
  "value": {
    "wait": 123
  }
}
new:2 - {
  "_id": "Exit",
  "_tag": "Success",
  "value": {
    "wait": 123
  }
}

使い方2

old - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "Sleep.Ng.Before"
    }
  }
}
new:1 - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "Sleep.Ng.Before"
    }
  }
}
new:2 - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "Sleep.Ng.Before"
    }
  }
}

使い方3

old - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "Sleep.Ng.After",
      "wait": 123
    }
  }
}
new:1 - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "Sleep.Ng.After",
      "wait": 123
    }
  }
}
new:2 - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "Sleep.Ng.After",
      "wait": 123
    }
  }
}

使い方4

time : 2025-02-27T12:47:45.648Z - text: mapError_sleepNg : UnknownException : UnknownException: An unknown error occurred
old - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "UnknownException",
      "error": {},
      "message": "An unknown error occurred",
      "cause": {}
    }
  }
}
time : 2025-02-27T12:47:45.652Z - text: liftMapTags : UnknownException : UnknownException: An unknown error occurred
new:1 - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "UnknownException",
      "error": {},
      "message": "An unknown error occurred",
      "cause": {}
    }
  }
}
time : 2025-02-27T12:47:45.652Z - text: liftMapTagsWithType : UnknownException : UnknownException: An unknown error occurred
new:2 - {
  "_id": "Exit",
  "_tag": "Failure",
  "cause": {
    "_id": "Cause",
    "_tag": "Fail",
    "failure": {
      "_tag": "UnknownException",
      "error": {},
      "message": "An unknown error occurred",
      "cause": {}
    }
  }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?