effect-tsでmapErrorTagsのようなもの

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

# 前回

https://qiita.com/kuangzhixitian3/items/4499c58bf7d4108791b1

## 共通部

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

```ts
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を持っているかだけの判断をしたいだけだったが、
型情報を持たせたりする必要があるかどうかわからず、こうなってしまった。

```ts
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
}
```

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

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

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



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

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

```ts
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
}
```

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

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

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

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

```ts
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
}
```

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

```ts
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　がエラーとして取得できる

## 実行確認

```ts
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('使い方１ - succeed', testOptions, async () => {
    const sleepEffect = Effect.succeed(new OkData(123))
    oldNew(sleepEffect)
  })
  test('使い方２ - fail - SleepNGBeforeError', testOptions, async () => {
    const sleepEffect = Effect.fail(new UnknownException(new SleepNGBeforeError()))
    oldNew(sleepEffect)
  })
  test('使い方３ - fail - SleepNGAfterError', testOptions, async () => {
    const sleepEffect = Effect.fail(new UnknownException(new SleepNGAfterError(123)))
    oldNew(sleepEffect)
  })
  test('使い方４ - fail - UnknownException', testOptions, async () => {
    const sleepEffect = Effect.fail(new UnknownException(new Error()))
    oldNew(sleepEffect)
  })
})
```

使い方１
```ts
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
  }
}
```

使い方２
```ts
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"
    }
  }
}
```

使い方３
```ts
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
    }
  }
}
```

使い方４
```ts
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": {}
    }
  }
}
```