LoginSignup
0
0

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

Last updated at Posted at 2024-04-16

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

前提条件

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

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

前回

複数の値のチェック

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

サンプルに近いやり方

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が2つ存在する

[
  {
    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の導入

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))
  })
})

実行結果

[
  {
    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
]

2つの実行結果の差分

[
  {
    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
]

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

複合的な値チェック方法1(参照値がそれぞれ1つの場合)

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

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('複合的 - 参照値1つ', () => {
    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))
  })
})

実行結果

[
  {
    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
]

複合的な値チェック方法2(参照値が複数の場合)

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

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))
  })
})

実行結果

[
  {
    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
]
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