入力チェック２【Vue】【io-ts】

io-tsによる入力チェックの実装による差分を確認する

# 前提条件

以下のような値をチェックすることを試す

```ts
const data = {
    name: "Test User",
    age: 34,
    movie: "CODE_012"
}
```

# 前回

https://qiita.com/kuangzhixitian3/items/98e3fa98ba2cfbdcca2d

複数の値のチェック

https://codesandbox.io/p/devbox/20240410-io-ts-3njpzr

# シンプルな値チェック方法の確認

### サンプルに近いやり方

https://github.com/gcanti/io-ts/blob/master/index.md

```ts
import { describe, it } from 'vitest'
import * as t from 'io-ts'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts入力チェック', () => {
  it('シンプル', () => {
    const FormCheckType = t.type({
      name: t.string,
      age: t.number
    })
    const data = {
      name: undefined,
      age: undefined
    }
    const debug_log = (x: any) => {
      console.log('%o', x)
      return x
    }
    const result = FormCheckType.decode(data)
    pipe(result, E.foldW(debug_log, debug_log))
  })
})
```

実行結果
name, age
それぞれにcontextが２つ存在する

```ts
[
  {
    value: undefined,
    context: [
      {
        key: '',
        type: InterfaceType {
          name: '{ name: string, age: number }',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          props: [Object],
          _tag: 'InterfaceType'
        },
        actual: { name: undefined, age: undefined }
      },
      {
        key: 'name',
        type: StringType {
          name: 'string',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          _tag: 'StringType'
        },
        actual: undefined
      },
      [length]: 2
    ],
    message: undefined
  },
  {
    value: undefined,
    context: [
      {
        key: '',
        type: InterfaceType {
          name: '{ name: string, age: number }',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          props: [Object],
          _tag: 'InterfaceType'
        },
        actual: { name: undefined, age: undefined }
      },
      {
        key: 'age',
        type: NumberType {
          name: 'number',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          _tag: 'NumberType'
        },
        actual: undefined
      },
      [length]: 2
    ],
    message: undefined
  },
  [length]: 2
]
```

### エラーメッセージやNonEmptyStringの導入

```ts
import { describe, it } from 'vitest'
import * as t from 'io-ts'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'
import { withMessage } from 'io-ts-types/lib/withMessage'
import { NonEmptyString } from 'io-ts-types/lib/NonEmptyString'

describe('fp-ts入力チェック', () => {
  it('シンプル - エラーメッセージ付き', () => {
    const FormCheckType = t.type({
      name: withMessage(NonEmptyString, () => '名前を入力してください'),
      age: withMessage(t.number, () => '年齢を入力してください')
    })
    const data = {
      name: undefined,
      age: undefined
    }
    const debug_log = (x: any) => {
      console.log('%o', x)
      return x
    }
    const result = FormCheckType.decode(data)
    pipe(result, E.foldW(debug_log, debug_log))
  })
})
```

実行結果

```ts
[
  {
    value: undefined,
    context: [
      {
        key: '',
        type: InterfaceType {
          name: '{ name: NonEmptyString, age: number }',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          props: [Object],
          _tag: 'InterfaceType'
        },
        actual: { name: undefined, age: undefined }
      },
      {
        key: 'name',
        type: RefinementType {
          name: 'NonEmptyString',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          type: [StringType],
          predicate: [Function],
          _tag: 'RefinementType'
        },
        actual: undefined
      },
      [length]: 2
    ],
    message: '名前を入力してください',
    actual: undefined
  },
  {
    value: undefined,
    context: [
      {
        key: '',
        type: InterfaceType {
          name: '{ name: NonEmptyString, age: number }',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          props: [Object],
          _tag: 'InterfaceType'
        },
        actual: { name: undefined, age: undefined }
      },
      {
        key: 'age',
        type: NumberType {
          name: 'number',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          _tag: 'NumberType'
        },
        actual: undefined
      },
      [length]: 2
    ],
    message: '年齢を入力してください',
    actual: undefined
  },
  [length]: 2
]
```

## ２つの実行結果の差分

```diff
[
  {
    value: undefined,
    context: [
      {
        key: '',
        type: InterfaceType {
-          name: '{ name: string, age: number }',
+          name: '{ name: NonEmptyString, age: number }',          
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          props: [Object],
          _tag: 'InterfaceType'
        },
        actual: { name: undefined, age: undefined }
      },
      {
        key: 'name',
-        type: StringType {
-          name: 'string',
+        type: RefinementType {
+          name: 'NonEmptyString',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
-          _tag: 'StringType'
+          type: [StringType],
+          predicate: [Function],
+          _tag: 'RefinementType'
        },
        actual: undefined
      },
      [length]: 2
    ],
-    message: undefined
+    message: '名前を入力してください',
+    actual: undefined
  },
  {
    value: undefined,
    context: [
      {
        key: '',
        type: InterfaceType {
-          name: '{ name: string, age: number }',
+          name: '{ name: NonEmptyString, age: number }',          
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          props: [Object],
          _tag: 'InterfaceType'
        },
        actual: { name: undefined, age: undefined }
      },
      {
        key: 'age',
        type: NumberType {
          name: 'number',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function],
          _tag: 'NumberType'
        },
        actual: undefined
      },
      [length]: 2
    ],
-    message: undefined
+    message: '年齢を入力してください',
+    actual: undefined
  },
  [length]: 2
]
```

# 複合的な値チェック方法の確認

### 複合的な値チェック方法１（参照値がそれぞれ１つの場合）

前回の複数の値のやり方
前回からの変更点としてmovieAgeRefinementのcontextを変えて
keyの値を取得するようにしている


```ts
import { describe, it } from 'vitest'
import * as t from 'io-ts'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts入力チェック', () => {
  it('複合的 - 参照値１つ', () => {
    type T_keyof_T<T> = T[keyof T]
    const getValueOfKeyOf =
      <T extends {}, U extends T_keyof_T<T_keyof_T<T>> & (string | number | symbol)>(value: T) =>
      (apply: (a: T[keyof T]) => U) => {
        const applyList: U[] = []
        Object.keys(value).forEach((key) => {
          const getValue = apply(value[key as keyof T])
          applyList.push(getValue)
        })

        const resultDictionary: Record<U, null> = {} as Record<U, null>
        for (const item of applyList) {
          resultDictionary[item] = null
        }

        return resultDictionary
      }
    const getKeyFromValue =
      <T extends {}, U extends T_keyof_T<T_keyof_T<T>> & (string | number | symbol)>(value: T) =>
      (fa: (a: T[keyof T]) => U) =>
      (target: U) => {
        let target_key = undefined
        Object.keys(value).forEach((key) => {
          const getValue = fa(value[key as keyof T])
          if (target === getValue) target_key = key
        })

        if (target_key) return E.right<never, keyof typeof MOVIE_TITLE>(target_key)
        return E.left(Error(`Not Found Key : ${String(target)}`))
      }
    const MOVIE_TITLE = {
      'Star Wars': { R_age: 5, code: 'CODE_001' },
      'Vanilla Sky': { R_age: 10, code: 'CODE_002' },
      'Atomic Blonde': { R_age: 15, code: 'CODE_003' }
    } as const
    type codeValueOfType = (typeof MOVIE_TITLE)[keyof typeof MOVIE_TITLE]['code']
    // const FormCheckType = t.type({
    //   name: withMessage(NonEmptyString, () => '名前を入力してください'),
    //   age: withMessage(t.number, () => '年齢を入力してください'),
    //   movie: withMessage(
    //     t.keyof(getValueOfKeyOf<typeof MOVIE_TITLE, codeValueOfType>(MOVIE_TITLE)((a) => a.code)),
    //     () => '映画のタイトルを入力してください'
    //   )
    // })
    const _movieAgeCodec = t.type(
      {
        age: t.number,
        movie: t.keyof(
          getValueOfKeyOf<typeof MOVIE_TITLE, codeValueOfType>(MOVIE_TITLE)((a) => a.code)
        )
      },
      'MovieAge'
    )
    type MovieAge = t.TypeOf<typeof _movieAgeCodec>
    const movieAgeRefinement = new t.Type<MovieAge, MovieAge>(
      'MovieAge',
      _movieAgeCodec.is,
      (input: any): E.Either<t.Errors, MovieAge> => {
        const movie_code = input.movie
        const age = input.age
        const key_check = getKeyFromValue(MOVIE_TITLE)((a) => a.code)(movie_code)

        return pipe(
          key_check,
          E.fold(
            (e: Error) => {
              return t.failure(
                input,
                [
                  {
                    key: 'age',
                    type: movieAgeRefinement,
                    actual: input
                  }
                ],
                e.message
              )
            },
            (a: keyof typeof MOVIE_TITLE) => {
              if (MOVIE_TITLE[a]['R_age'] > age)
                return t.failure(
                  input,
                  [
                    {
                      key: 'age',
                      type: movieAgeRefinement,
                      actual: input
                    }
                  ],
                  `${MOVIE_TITLE[a]['R_age']}歳未満の年齢の人はこの映画を選択できません`
                )
              else return t.success(input)
            }
          )
        )
      },
      _movieAgeCodec.encode
    )

    const data = {
      name: 'Test User',
      age: 3,
      movie: 'CODE_001'
    }
    const debug_log = (x: any) => {
      console.log('%o', x)
      return x
    }
    const result = movieAgeRefinement.decode(data)
    pipe(result, E.foldW(debug_log, debug_log))
  })
})
```

実行結果

```ts
[
  {
    value: { name: 'Test User', age: 3, movie: 'CODE_001' },
    context: [
      {
        key: 'age',
        type: Type {
          name: 'MovieAge',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function]
        },
        actual: { name: 'Test User', age: 3, movie: 'CODE_001' }
      },
      [length]: 1
    ],
    message: '5歳未満の年齢の人はこの映画を選択できません'
  },
  [length]: 1
]
```

### 複合的な値チェック方法２（参照値が複数の場合）

keyの値が設定されると思ったが
設定されず、あまりメリットがない記述方法

```ts
import { describe, it } from 'vitest'
import * as t from 'io-ts'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'

describe('fp-ts入力チェック', () => {
  it('複合的 - 参照値が複数', () => {
    type T_keyof_T<T> = T[keyof T]
    const getValueOfKeyOf =
      <T extends {}, U extends T_keyof_T<T_keyof_T<T>> & (string | number | symbol)>(value: T) =>
      (apply: (a: T[keyof T]) => U) => {
        const applyList: U[] = []
        Object.keys(value).forEach((key) => {
          const getValue = apply(value[key as keyof T])
          applyList.push(getValue)
        })

        const resultDictionary: Record<U, null> = {} as Record<U, null>
        for (const item of applyList) {
          resultDictionary[item] = null
        }

        return resultDictionary
      }
    const getKeyFromValue =
      <T extends {}, U extends T_keyof_T<T_keyof_T<T>> & (string | number | symbol)>(value: T) =>
      (fa: (a: T[keyof T]) => U) =>
      (target: U) => {
        let target_key = undefined
        Object.keys(value).forEach((key) => {
          const getValue = fa(value[key as keyof T])
          if (target === getValue) target_key = key
        })

        if (target_key) return E.right<never, keyof typeof MOVIE_TITLE>(target_key)
        return E.left(Error(`Not Found Key : ${String(target)}`))
      }
    const MOVIE_TITLE = {
      'Star Wars': { R_age: 5, code: 'CODE_001' },
      'Vanilla Sky': { R_age: 10, code: 'CODE_002' },
      'Atomic Blonde': { R_age: 15, code: 'CODE_003' }
    } as const
    type codeValueOfType = (typeof MOVIE_TITLE)[keyof typeof MOVIE_TITLE]['code']
    const _movieAgeCodec = t.type({
      age: t.type({
        age: t.number,
        movie: t.keyof(
          getValueOfKeyOf<typeof MOVIE_TITLE, codeValueOfType>(MOVIE_TITLE)((a) => a.code)
        )
      })
    })
    type MovieAge = t.TypeOf<typeof _movieAgeCodec>
    const movieAgeRefinement = new t.Type<MovieAge, MovieAge>(
      'MovieAge',
      _movieAgeCodec.is,
      (input: any, context: t.Context): E.Either<t.Errors, MovieAge> => {
        const movie_code = input.age.movie
        const age = input.age.age
        const key_check = getKeyFromValue(MOVIE_TITLE)((a) => a.code)(movie_code)

        return pipe(
          key_check,
          E.fold(
            (e: Error) => {
              return t.failure(input, context, e.message)
            },
            (a: keyof typeof MOVIE_TITLE) => {
              if (MOVIE_TITLE[a]['R_age'] > age)
                return t.failure(
                  input,
                  context,
                  `${MOVIE_TITLE[a]['R_age']}歳未満の年齢の人はこの映画を選択できません`
                )
              else return t.success(input)
            }
          )
        )
      },
      _movieAgeCodec.encode
    )

    const data = {
      name: 'Test User',
      age: {
        age: 3,
        movie: 'CODE_001'
      },
      movie: 'CODE_001'
    }
    const debug_log = (x: any) => {
      console.log('%o', x)
      return x
    }
    const result = movieAgeRefinement.decode(data)
    pipe(result, E.foldW(debug_log, debug_log))
  })
})
```

実行結果

```ts
[
  {
    value: {
      name: 'Test User',
      age: { age: 3, movie: 'CODE_001' },
      movie: 'CODE_001'
    },
    context: [
      {
        key: '',
        type: Type {
          name: 'MovieAge',
          is: [Function],
          validate: [Function],
          encode: [Function],
          decode: [Function]
        },
        actual: { name: 'Test User', age: [Object], movie: 'CODE_001' }
      },
      [length]: 1
    ],
    message: '5歳未満の年齢の人はこの映画を選択できません'
  },
  [length]: 1
]
```