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
]