Published on

yup docs 에 대한 정리

Authors
  • avatar
    Name
    길재훈
    Twitter

yup 에 대한 정리

react-hook-form 을 사용하면서, yupresolver 로 사용하고 있다.
yup 을 사용하면서 기껏해야 yup.stringyup.email, yup.objact 등등...

yup 에서 제공하고 있는 많은 기능중, 한정적으로 사용하고 있다는 생각이 들어 내용 정리차, blog 에 기록해두려 한다.

yup 이란?

다음은 docs 에 적혀져 있는 문구이다.

Yupruntime 에 값 분석 및 유효성검사를 하기 위한 schema builder 이다.

이말은, schema 를 작성후, 값을 평가하여, 해당 값이 정확하게 맞아 떨어지는지를 검사하고 평가한다.

이러한 평가하는 방식은, input 사용시 꼭 필요한 기능이다. 그러면서 다음처럼 같이 설명해준다.

Yup schema 는 매우 표현력이 좋으며, 값 변환 및 의존성있는 값들의 평가 같은 복잡한 modeling 을 허용한다.

위의말은, 유연하게 사용할수 있는 method 들이 많으며, 그로인해 유효성검사를
쉽게 할수 있다 정도로 이해하면 될것 같다.

Schema 란?

Schematype 이 디자인된 형태라고 보면된다. Schema 를 통해, input 을 조작할 수 있다.

이러한 Schemamethod chian 형태로, 각 기능들을 나열하여 처리 가능하다. 아래는 Docs 에 나와 있는 예시이다.

const num = number().cast('1') // 1

const obj = object({
  firstName: string().lowercase().trim(),
})
  .json()
  .camelCase()
  .cast('{"first_name": "jAnE "}') // { firstName: 'jane' }

위의 예시는 나열되어 있는 예시는, json.parser
진행하고, objectcacmelCase 로 만들라고 하는 명령이다.

cast 는 어떠한 값을 넣으면, 위의 나열된 method chain 으로 변환해주는
함수라고 생각하면 된다.

실제로 위의 값으로 JSON 객체를 넣었는데, 결과값으로, Object 를 반환하는것을 볼 수 있다.

Yup 은 이렇게 input 사용시 발생할 수 있는 문제들을 해결하기 위해 여러 method 들을 제공하며, typescript 역시 지원해 편리한 사용이 가능하다.

docs 에서는 custom transrom 을 만들어 처리도 가능하다고 말한다.

const reversedString = string()
  .transform((currentValue) => currentValue.split('').reverse().join(''))
  .cast('dlrow olleh') // "hello world"

여기서, transform 을 사용해 안쪽에 함수를 전달해주면, Yup 에서
직접 만든 method 를 사용하여 조작 가능하다.

다른 예시도 많으므로, Docs 에서 살펴보는것이 좋을듯 싶다.

Typescript integration

Yup 은 정적 Typescript interface 생성이 가능하다. 다음을 보자.

import * as yup from 'yup'

const personSchema = yup.object({
  firstName: yup.string().defined(),
  nickName: yup.string().default('').nullable(),
  sex: yup
    .mixed()
    .oneOf(['male', 'female', 'other'] as const)
    .defined(),
  email: yup.string().nullable().email(),
  birthDate: yup.date().nullable().min(new Date(1900, 0, 1)),
})

interface Person extends yup.InferType<typeof personSchema> {
  // using interface instead of type generally gives nicer editor feedback
}

interface Personyup.InferType 을 사용하여, type 을 추론한후 확장하여 지정한다.

또한 Schemadefault 값을 줄수 있다고도 되어있다.

import { string } from 'yup'

const value: string = string().default('hi').validate(undefined)

// vs

const value: string | undefined = string().validate(undefined)

Yuptypescript 를 지원한다고 했으므로, 역시나 interface 를 지정하여, 해당 interfaceyup 에 넘겨 처리할 수 도 있다.

import { object, number, string, ObjectSchema } from 'yup';

interface Person {
  name: string;
  age?: number;
  sex: 'male' | 'female' | 'other' | null;
}

// will raise a compile-time type error if the schema does not produce a valid Person
const schema: ObjectSchema<Person> = object({
  name: string().defined(),
  age: number().optional(),
  sex: string<'male' | 'female' | 'other'>().nullable().defined();
});

// ❌ errors:
// "Type 'number | undefined' is not assignable to type 'string'."
const badSchema: ObjectSchema<Person> = object({
  name: number(),
});

API

API 는 다음의 module 형식을 따른다고 한다.

// core schema
import { mixed, string, number, boolean, bool, date, object, array, ref, lazy } from 'yup'

// Classes
import {
  Schema,
  MixedSchema,
  StringSchema,
  NumberSchema,
  BooleanSchema,
  DateSchema,
  ArraySchema,
  ObjectSchema,
} from 'yup'

// Types
import type { InferType, ISchema, AnySchema, AnyObjectSchema } from 'yup'

reach

reachnested schemas 중에 제공된 path 를 기반으로, 그 값을 찾는다.

import { reach } from 'yup'

let schema = object({
  nested: object({
    arr: array(object({ num: number().max(4) })),
  }),
})

reach(schema, 'nested.arr.num')
reach(schema, 'nested.arr[].num')
reach(schema, 'nested.arr[1].num')
reach(schema, 'nested["arr"][1].num')

ref

ref 는 필드의 자속 혹은 형제를 참조하는 field 를 만들 수 있다.
아래를 보도록 하자.

import { ref, object, string } from 'yup'

let schema = object({
  baz: ref('foo.bar'),
  foo: object({
    bar: string(),
  }),
  x: ref('$x'),
})

schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } })
// => { baz: 'boom',  x: 5, foo: { bar: 'boom' } }

위의 예를 보면, bazref 를 통해, 자신의 형제 필드의 자손인 foo.bar 의 값을 참조하는것을 볼 수 있다.

여기서, $x 의 의미가 중요한다, Yup 에서는 $ 기호를 이용하여, 컨텍스트의 값을 참조 할 수 있다.

위의 예시를 보면, { context: { x: 5 } } 를 통해, 추가정보를를 전달하기 위해 contextx 값을 넣었고, schema 에서는 contextx 를 사용하기 위해 $ 를 사용하여 참조하고 있다.


context

schema 검증시 추가 정보를 전달하기 위한 객체이다.
contextschema 검증시, 검증 함수에 전달되고, 해당 검증 함수에서 "$" 를 사용해서 contextfield 를 참조한다.

context 는 다음의 proerties 로 이루어져 있다.

"value", "path", "label


ref 의 중요한 부분중 하나는, 자기 자신의 field 를 참조할 수 있다는 것이다.

이러한 부분은 circuler dependencies 가 발생할 수 있으니, 조심스럽게 사용해야 한다.

lazy

유효성 검사 / 캐스트 시간에 평가되는 스키마

Docs 에서는 이렇게 말하고 있다.
이는 나중에 평가되는 schema 라고 해서 lazy 라고 명명되어진것 같다.

methodpolymorphic(다형성)array 같은 Tree 를 가진 Schema 에서 재귀적으로 생성하는데 유용하다고 되어 있다.

사실 그렇게까지는 와닿지는 않은 방식이라, 써보면서 익혀야 될것 같다.

let node = object({
  id: number(),
  child: yup.lazy(() => node.default(undefined)),
})

let renderable = yup.lazy((value) => {
  switch (typeof value) {
    case 'number':
      return number()
    case 'string':
      return string()
    default:
      return mixed()
  }
})

let renderables = array().of(renderable)

ValidationError

이름을 보면 알겠지만, 유효성 검사 오류를 지정해주는 함수이다. 유효성 검사가 실패했을때 error 를 던진다. Docs 에서는 다음처럼 이루어져 있다고 한다.

  • name: "ValidationError"
  • type: 실패된 test이름 또는 type 을 지정한다.
  • value: 테스트된 fieldvalue 값이다.
  • params ?: test inpu 들이다, 에를 들어, max value, regex 같은...
  • errors: error message 들의 배열이다.
  • inner: 집계 오류인 경우, 유효성 검사 chain 초기에 발생하는, ValidataionErrors 의 배열이다.
    abortEarly 옵션이 false일때, 발생한 각 오류를 검사할 수 있다. 이 오류에는 각 내부 오류의 모든 메시지가 포함된다.

Schema

schemabastract base class(추상 기반 class) 이다.
schemaschema type 에 기본 methodsproperties 를 제공한다.

Schema.clone(): Schema

Schemadeep copy 한다. Schema 복제는 상태 변경과 함께 새 스키마를 반환하기 위해 내부적으로 사용된다고 한다

Schema.label(label: string): Schema

error message 안의 사용된 key nameoverrinding 한다.

Schema.meta(metadata: object): Schema

metadata 객체를 추가한다 schema 와 함께 data 를 저장할때 유용하다 cast 객체 자신에게 포함되지 않는, data 를 저장할때 유용하다

Schema.describe(options?: ResolveOptions): SchemaDescription

직렬화된 description 객체안의 schema 의 세부정보 모음이다.

(like meta, labels, active tests)

Docs 에서의 예제는 다음과 같다

const schema = object({
  name: string().required(),
})

const description = schema.describe()

이렇게 하면 description 변수에는 schema 에 설정한 세부정보를 담은
객체를 가진다.

하지만, dynamic component 같은 경우는, 정확한 schema 설명을 리턴하기 위한 더 많은 context 를 서술해야 한다.

import { ref, object, string, boolean } from 'yup'

let schema = object({
  isBig: boolean(),
  count: number().when('isBig', {
    is: true,
    then: (schema) => schema.min(5),
    otherwise: (schema) => schema.min(0),
  }),
})

schema.describe({ value: { isBig: true } })

위의 when()isBig 의 값이 true 이면, countmin 값은 5 이고, 아니면 0 이다. 로 해석된다.

이렇게 describe 함수를 하용할때, 값이 dynamic 하게 변경되는 값이면,
context 를 작성하여, 처리해주어야 한다고 서술되어 있다.

해당 부분의 docs 를 보면서 개념적이해를 하고 있는 상황이라 실제로 써보면서 조금더 살펴보아야 할것 같다.

Docs 에서는 아래의 코드를 부여주면서 description types 라고 한다. 참고로, schema type 에 의존하여 약간씩 달라질 수 있다고 한다.

interface SchemaDescription {
  type: string
  label?: string
  meta: object | undefined
  oneOf: unknown[]
  notOneOf: unknown[]
  default?: unknown
  nullable: boolean
  optional: boolean
  tests: Array<{ name?: string; params: ExtraParams | undefined }>

  // Present on object schema descriptions
  fields: Record<string, SchemaFieldDescription>

  // Present on array schema descriptions
  innerType?: SchemaFieldDescription
}

type SchemaFieldDescription = SchemaDescription | SchemaRefDescription | SchemaLazyDescription

interface SchemaRefDescription {
  type: 'ref'
  key: string
}

interface SchemaLazyDescription {
  type: string
  label?: string
  meta: object | undefined
}

어허,, 이부분은 추후 더 살펴보아야 할것 같다..

Schema.concat(schema: Schema): Schema

2개의 schema 를 결합하여 새로운 schema 를 만든다 동일한 유형의 schemaconcatenated 한다

concatschema 를 재정의한다는 것에서 merge 기능이 아니라고 말한다.

import * as Yup from 'yup'

const schema1 = Yup.object().shape({
  name: Yup.string().required(),
  age: Yup.number().integer().min(18).max(99),
})

const schema2 = Yup.object().shape({
  email: Yup.string().email().required(),
  password: Yup.string().matches(/^[a-zA-Z0-9]{3,30}$/),
})

const mergedSchema = schema1.concat(schema2)

Schema.validate(value: any, options?: object): Promise<InferType<Schema>, ValidationError>

input value 의 유효성을 평가하고 리턴한다. 리턴될 값은 평가된 값 혹은 throwing error 이다.

method 는 비동기적이고, promise object 를 반환한다. 이 promise objectvalue 와 함께 이행(fullfilled) 되거나,ValidationError 와 함께 거부(reject) 된다.

value = await schema.validate({ name: 'jimmy', age: 24 })

options 는 좀더 구체적으로 조작하기위해 제공한다.

interface Options {
  // when true, parsing is skipped an the input is validated "as-is"
  strict: boolean = false
  // Throw on the first error or collect and return all
  abortEarly: boolean = true
  // Remove unspecified keys from objects
  stripUnknown: boolean = false
  // when `false` validations will be performed shallowly
  recursive: boolean = true
  // External values that can be provided to validations and conditionals
  context?: object
}

위의 제공되는 Options 를 제공한다.

Schma.validateSync(value: any, options?: object): InferType<Scehma>

동기적으로 validations 를 실행한다. 이는 결과를 반환하거나, ValidationErrorthrow 한다

options 는 위의 validate 와 동일하다

동기적 validationaync test 가 없는 경우에만 작동한다.
예를들어 Promise 를 반환하는 test 같은경우 다음과 같이 작동한다.

let schema = number().test('is-42', "this isn't the number i want", (value) =>
  Promise.resolve(value != 42)
)

schema.validateSync(42) // throws Error

다음은 Promise 를 반환하지 않는 test 이다.

let schema = number().test('is-42', "this isn't the number i want", (value) => value != 42)

schema.validateSync(23) // throws ValidationError

Schema.validateAt, ValidationError

중첩된 경로를 통해 검증하지만, 결과 schema 를 유효성 대상으로 사용한다.

let schema = object({
  foo: array().of(
    object({
      loose: boolean(),
      bar: string().when('loose', {
        is: true,
        otherwise: (schema) => schema.strict(),
      }),
    })
  ),
})

let rootValue = {
  foo: [{ bar: 1 }, { bar: 1, loose: true }],
}

await schema.validateAt('foo[0].bar', rootValue) // => ValidationError: must be a string

await schema.validateAt('foo[1].bar', rootValue) // => '1'

Schema.validateSyncAt

이는 validateAt 의 동기적 버전이다.

Schema.isValid

주어진 valueschema 와 일치하면 true 를 리턴한다. isValidasunchronous 이므로, promise 객체를 반환한다.

Schema.isValidSync

isValid동기적 버전이다.

Schema.cast

주어진 valueSchema 와 강제로 일치하기 위해 시도한다. 예를 들어, number() 타입을 사용할때 '5'5 로 변환한다.

cast 가 실패되면 일반적으로 null 을 반환한다. 그러나, 유효하지않은 string 이라면 NaN 같은 결과를 리턴할수도 있다.

설정할 수 있는 값들 역시 같이 제공한다.

interface CastOptions<TContext extends {}> {
  // Remove undefined properties from objects
  stripUnknown: boolean = false

  // Throws a TypeError if casting doesn't produce a valid type
  // note that the TS return type is inaccurate when this is `false`, use with caution
  assert?: boolean = true

  // External values that used to resolve conditions and references
  context?: TContext
}

Schema.isType

전달된 value 에 대해서 type check 를 한다. 일치한다면 true를 반환하며, valuecast 하지는 않는다.

모든 Schema 타입을 체크하는데 isType 을 사용해야한다.

Schema.strip

출력 object 에서 제거할 schema 를 표시한다. 오직, 중첩된 schema 에서 작동한다.

let schema = object({
  useThis: number(),
  notThis: string().strip(),
})

schema.cast({ notThis: 'foo', useThis: 4 }) // => { useThis: 4 }

Docs 에서는 strip 이 적용된 schema 는 타입이 naver 으로 추론되므로, 모든 유형에서 제거된다고 한다.

let schema = object({
  useThis: number(),
  notThis: string().strip(),
})

InferType<typeof schema> /*
{
   useThis?: number | undefined
}
*/

Schema.default

valueundefined 일때, default value 를 설정한다.
default 는 변형이 실행된 이후에 생성된다.
이는 유효성검사 이전에 지정된 안전한 default 를 보장하는데 도움이 된다.

default value 는 사용될때마다, cloned 되어, objectarray 사용시 크기가 클 경우 성능 저하가 발생될수 있다.

overhead 를 방지하기 위해 새로운 default 를 반환하는 function 을 통해 값을 전달하는 방식도 존재한다.

그 값이 크지 않다면 기본값으로 설정해도 되지만, 객체의 크기가 클경우에는 function 으로 대체해서 처리하는것이 좋을듯 싶다

default 속성은 null 도 비어있는 값으로 간주한다. 그래서, 해당 필드가 누락된다면 null 이 아니라 "" 로 설정한다.

yup.string.default('nothing')

yup.object.default({ number: 5 }) // object will be cloned every time a default is needed

yup.object.default(() => ({ number: 5 })) // this is cheaper

yup.date.default(() => new Date()) // also helpful for defaults that change over time

Schema.getDefault

default value 를 설정한 값을 찾는데 사용된다.

Schema.nullable

null 을 가리킨다. nullable() 없으면 null 은 다른유형으로 취급된다. Schema.isType 으로 check 할때 실패할것이다.

const schema = number().nullable()

schema.cast(null) // null

InferType<typeof schema> // number | null

Schema.nonNullable

nullable() 과 반대된다. Schema 를 에서 유효한 유형의 값들로 부터 null 을 제거한다

Schma 는 기본적으로 null 을 허용하지 않는다

const schema = number().nonNullable()

schema.cast(null) // TypeError

InferType<typeof schema> // number

Schema.defined

Schema 에 대한 value을 반드시 정의해야 한다 모든 field 값들은 undefined 외의 모든 값은 이 요구사항에 충족한다.

이 말은 값은 반드시 지정되어야 하지만, null 값은 허용됨을 말한다.

const schema = string().defined()

schema.cast(undefined) // TypeError

InferType<typeof schema> // string

Schema.optional

defined() 의 반대이다. 제공되는 타입의 valueundefined 를 허용한다

const schema = string().optional()

schema.cast(undefined) // undefined

InferType<typeof schema> // string | undefined

Schema.required

Schemarequired 를 표시한다면, 값으로써 null, undefined 를 허용하지 않는다.

이는 defeind 와는 다르다. definednull 을 허용하지만, undefined 는 허용하지 않는다.

그러므로, requiredoptionalnallable 의 반대되는 method 이다.

Docs 에서 string().required 일때, null, undefined 뿐만 아니라 "" 빈 문자열도 허용하지 않으므로, 주의하라고 설명해준다.

Schema.notRequired

notRequired() 를 표시하면, schema.nullable().optional() 처럼 동작한다.

Schema.typeError

type check 가 실패될때, error message 를 정의한다. ${value} 그리고 ${type}message 인자 안에서 사용할 수 있다.

Schema.oneOf

오직, 값 집합의 값들만 허용한다. 이는 마치 배열처럼 동작하며, 해당 배열의 값을 isValid 함수를 통해 확인가능하다.

검증시 확인된 resolved valuesget 할때, ${value} 보간 및 refs 또는 ref 가 있다면, ${resolved} 보간은 message 인자안에서 보간으로 사용될 수 있다.

undefinedarrayOfValues 에 포함되지 않는 경우에도 검증에 실패되지 않는다.

만약, undefined 를 원치 않는다면, Schema.required 를 사용할 수 있다.

let schema = yup.mixed().oneOf(['jimmy', 42])

await schema.isValid(42) // => true
await schema.isValid('jimmy') // => true
await schema.isValid(new Date()) // => false

Schema.notOneOf

이는 oneOf() 에 반대되는 method 이다. 값의 집합에 있는 값은 허용하지 않는다.

let schema = yup.mixed().notOneOf(['jimmy', 42])

await schema.isValid(42) // => false
await schema.isValid(new Date()) // => true

Schema.when

형제 또는 형제의 자손 field 를 기반으로 한 schema 를 조정하는데 사용된다. 마치 case 문처럼 처리가 가능하며, 작동은 다음과 같다

when 에는 object literal 혹은 함수 를 넣을 수 있다.

objectis 라는 key 에는 value 또는 matcher function 을 제공한다. 이로써, key 에서 제공되는 값과 형제 및 형제의 자손 field 와 같아면, then 이 실행되고, 그렇지 않다면 otherwise 가 실행된다.

또한, is(엄격한 동등 연산자)=== 로 작동하며, 다른 방식을 원한다면 함수를 통해 설정가능하다

예) is: (value) => value == true

이는 다른 field 에 대한 의존하여 값이 설정될때 사용된다.

input value 대신에 context 에 연관된 property 를 지정할때는 $prefix 로 사용할 수 있다.

context 를 사용하고 싶다면, validatecast 를 사용하여 지정해야 한다.

let schema = object({
  isBig: boolean(),
  count: number()
    .when('isBig', {
      is: true, // alternatively: (val) => val == true
      then: (schema) => schema.min(5),
      otherwise: (schema) => schema.min(0),
    })
    .when('$other', ([other], schema) => (other === 4 ? schema.max(6) : schema)),
})

await schema.validate(value, { context: { other: 4 } }) // context 를 사용한 예

만약, dependant key 를 더 지정하고 싶다면, 배열을 사용한다. 이때, 배열안에 들어간 값은 && 처럼 사용된다.

let schema = object({
  isSpecial: boolean(),
  isBig: boolean(),
  count: number().when(['isBig', 'isSpecial'], {
    is: true, // alternatively: (isBig, isSpecial) => isBig && isSpecial
    then: (schema) => schema.min(5),
    otherwise: (schema) => schema.min(0),
  }),
})

await schema.validate({
  isBig: true,
  isSpecial: true,
  count: 10,
})

다르게 작성도 가능한데, function 을 사용해서 schema 를 리턴한다.

let schema = yup.object({
  isBig: yup.boolean(),
  count: yup.number().when('isBig', ([isBig], schema) => {
    return isBig ? schema.min(5) : schema.min(0)
  }),
})

await schema.validate({ isBig: false, count: 4 })

function 의 첫번쩨 인자는, 제공된 key 에대한 값의 배열이며, 두번째 인자는 현재 schema 를 제공한다.

Schema.test

필드값을 test 하는 메소드

objectcasing 된 이후에 테스트가 실행된다. 이러한 testcustom 하게 제공하는 기능을 제공한다.

yup 에서는 비동기식으로 유효성검사가 이루어지기에, 모든 테스트가
비동기식으로 실행된다.

비동기는 굉장히 유용한 기능이지만, 단점은, 실행순서를 보장할 수 없다. 그래서, 동기식으로 test 하는법 역시 존재한다.

모든 테스트는 name, error message, 현재 값이 유효하면 그때, true 를 반환하고, 그렇지 않으면 falsevalidationError 를 반환하는 함수를 제공해야 한다.

만약, 비동기식 이라면 true, false 혹은 validationError 를 반환하는 promise 를 반환한다.

message 인수의 경우 ${param} 구문을 사용하여 지정된 경우 특정값을 보간하는 문자열을 제공할 수 있다.

기본적으로 모든 테스트 메시지는 중첩된 스키마에서 중요한 경로 값을 전달한다고 한다.

다음을 보자.

let jimmySchema = string().test(
  'is-jimmy',
  '${path} is not Jimmy',
  (value, context) => value === 'jimmy'
)

// or make it async by returning a promise
let asyncJimmySchema = string()
  .label('First name')
  .test(
    'is-jimmy',
    ({ label }) => `${label} is not Jimmy`, // a message can also be a function
    async (value, testContext) => (await fetch('/is-jimmy/' + value)).responseText === 'true'
  )

await schema.isValid('jimmy') // => true
await schema.isValid('john') // => false

test 함수는 2번째 인자의 특별한 context value 와 함께 호출된다. 이는 metadata 로 유용하게 사용된다.

test contextarrow function 이 아닌 function 을 사용할경우 this 가 향하는 객체는 test context 가 된다.

arrow function 에는 this 참조는 lexical context 를 그대로 계승받는다. 일반 함수와는 다르게 동적이지 않고 정적으로 향하므로, this 를 통해 접근이 불가하다.

실제 functionbind 함수를 통해, 객체 바인딩이 가능하지만, arrow function 은 이를 지원하지 않는다.

이러한 연유로 arrow function 에서는 this 에 대한 참조로 접근하지 않는듯 하다.

textContext 는 다음의 property 를 갖는다.

  • textContext.path: 현재 유효성검사하는 경로
  • textContext.schema: test 실행하는데 확인된 schema object
  • testContext.options: options 객체는 validate() 혹은 isValid() 와 함께 호출된다.
  • testContext.originalValue: test 중인 original value = testContext.createError(Object: { path: String, message: String, params: Object}): 리턴할 validation error 를 생성한다. 이는 동적으로 setting 하기에 유용하다. 만약 생략된다면 current path, default message 가 사용된다.

Schema.test(options: object): Schema

Schema 를 검증하는 메소드

이는 Schema 를 검증하는데 사용된다. 이를 위해서는 options 를 사용하여 설정가능하다.

options 는 다음과 같다.

Options = {
  // unique name identifying the test
  name: string;
  // test function, determines schema validity
  test: (value: any) => boolean;
  // the validation error message
  message: string;
  // values passed to message for interpolation
  params: ?object;
  // mark the test as exclusive, meaning only one test of the same name can be active at once
  exclusive: boolean = false;
}

여기서 exclusive 가 눈에 띄는데, 이는 다음과 같다.

  • Exclusive Test: 스키마 객체의 모든 필드가 검증 규칙을 통과해야 한다. 이중 하나라도 검증이 실패하면 전체 검증이 실패한다.

  • Non-Exclusive Test: 스키마 객체의 각 필드는 개별적으로 검증되며, 검증 규칙 중 하나라도 실패하더라도 다른 필드들은 여전히 검증된다.

Schema.transform

Schema 객체의 값을 변환하는데 사용되는 메서드이다. 이는 validate 메서드가 호출되기 전에 값을 변환한다.

transform 메서드는 인수를 받는데, currentValueschema 객체 의 값이며, originalValue 는 원래의 값을 가진 인수이다.

이는 다음처럼 사용가능하다.

const schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required(),
})
const transformedSchema = schema.transform(function (obj, originalValue) {
  if (obj.age < 0) {
    return { ...obj, age: 0 } // age 값이 음수일 경우 0으로 변환
  }
  return obj // 변환하지 않음
})

위는 age 필드의 값이 음수일 경우 age0 이 되고,
그렇지 않으면 obj 그대로를 반환한다.

이러한 방식대로 처리 가능하다.

mixed

mixed

이는 Schema 로 부터 상속받는 타입으로, 모든 타입에 일치한다.
이에 대한 타입을 보면 mixed extends {}any 또는 unknown 대신에 사용된다.

typescript 에서 {}null 혹은 undefined 를 제외한 모든 타입을 의미한다.

import { mixed, InferType } from 'yup';

let schema = mixed().nullable();

schema.validateSync('string'); // 'string';

schema.validateSync(1); // 1;

schema.validateSync(new Date()); // Date;

InferType<typeof schema>; // {} | undefined

InferType<typeof schema.nullable().defined()>; // {} | null

밑의 예는 Docs 에서 설명해주는 code 이다.

import { mixed, InferType } from 'yup'

let objectIdSchema = yup
  .mixed((input): input is ObjectId => input instanceof ObjectId)
  .transform((value: any, input, ctx) => {
    if (ctx.isType(value)) return value
    return new ObjectId(value)
  })

await objectIdSchema.validate(ObjectId('507f1f77bcf86cd799439011')) // ObjectId("507f1f77bcf86cd799439011")

await objectIdSchema.validate('507f1f77bcf86cd799439011') // ObjectId("507f1f77bcf86cd799439011")

InferType<typeof objectIdSchema> // ObjectId

위는 mixed 타입을 가지며, mixed 타입의 값이 ObjectId 값이 맞으면, value 를 리턴하고, 그렇지 않으면 ObjectId 로 만들어 리턴한다.

mixed 에서는 함수를 인자로 받아 유효성 검사를 수행할 수 있다. 이때 반환된 값은 해당 Schema 에 부합하는지 여부를 검사하고, true 혹은 false 를 반환한다.

const schema = yup.mixed((value) => {
  // 유효성 검사 로직
  return true // 유효한 값인 경우 true 반환
})

또는, 타입가드 함수를 전달할수도 있다.

const schema = yup.mixed((input): input is MyType => {
  // 타입 가드 로직
  return true // 입력값이 MyType인 경우 true 반환
})

mixed 에서 이부분을 사용하여, type guard 를 실행해, 해당 타입임을 검증한다. mixed 에서 이부분을 사용하여, type guard 를 실행해, 해당 타입임을 검증한다.

여기서는 transform 을 사용하여 검증이전에 값을 변경하고, 검증이 이루어진다검증이 이루어진다.

위의 인자은 다음과 같다.

  • value: 변환할 값
  • input: 변환이전의 값
  • ctx: Yup 컴텍스트 객체

ctx.isTypeYup 에서 isType 을 가져온것이다. isTypemix 값이 ObjextId 라면, valueObjextId 인지 확인한다.

이러한 로직을 통해, 위의 code 가 제대로 검증처리가 됨을 알 수 있다.

string

이는 string 타입을 검사하는 로직이다. 내부적으로 cast 가 실행되고 이는 toString 과 같다

string.required

빈 문자열도 누락되는점을 제외하고는 mixed().required() 와 동일하다.

string.length

문자열 값의 length 을 요구되도록 설정한다. ${length} 보간은 message 인자에서 사용될수 있다

string.min

string value 의 값을 최소 length 로 제한한다. ${min} 보간은 message 인자에서 사용될수 있다

string.max

string value 의 값을 최대 length 로 제한한다. ${max} 보간은 message 인자에서 사용될수 있다

string.matches

임의의 정규식을 사용하여, value 와 일치하는지 검사한다

let schema = string().matches(/(hi|bye)/)

await schema.isValid('hi') // => true
await schema.isValid('nope') // => false

string.matches

matches 와 같지만 추가적 options 를 추가할 수 있다. excludeEmptyString"" 빈문자열일때, 패턴 검사를 건너띄고 유효한 값으로 처리한다.

이는 빈문자열일 수 있는 상황에서 유효하게 사용될 것이다.

let schema = string().matches(/(hi|bye)/, { excludeEmptyString: true })

await schema.isValid('') // => true

string.email

email 값인지 확인한다. 이는 HTML spec 에서 지정한 regex 와 같다.

하지만, eamil 에 대해서는 워낙에 다양하기에 꼭 맞아 떨아진다고 보장하기 어렵다.

이러한 경우 처리할 수 있도록 아래의 방식을 제안한다.

yup.addMethod(yup.string, 'email', function validateEmail(message) {
  return this.matches(myEmailRegex, {
    message,
    name: 'email',
    excludeEmptyString: true,
  })
})

이부분에 대해서 addMethod 를 통해 email 의 양식을 평가할 수 있는, regexp 를 만든다.

string.url

URL 에 대한 regex 를 검증한다.

string.uuid

UUID 에 대한 regex 를 검증한다.

string.ensure

undefined 그리고 null 값을 빈 문자열로 변환하고, default 로 설정한다.

string.trim

선행 및 후행의 빈 문자열을 제거한다. 만약, strinc() 이면 값이 잘리는지 확인만 한다.

string.lowercase

lowercase 로 변환한다. 만약 strict() 이면, lowcase 인지만 확인한다.

string.uppercase

uppercase 로 변환한다. 만약 strict() 이면, uppercase 인지만 확인한다.

number

number schema 를 정의한다 주의할점은 parseFloat 을 통해 cast 한다는 것이다. 그러므로, 실패시 NaN 을 리턴할 수 도 있다.

let schema = yup.number()

await schema.isValid(10) // => true

number.min

최소값을 설정한다. 최소값을 설정한다. ${min} 보간은 message 인자에서 사용될수 있다

number.max

최대값을 설정한다. 최대값을 설정한다. ${max} 보간은 message 인자에서 사용될수 있다

number.lessThan

값이 max 보다 적어야만 한다 ${less} 보간은 message 인자에서 사용될수 있다

number.moreThan

값이 min 보다 적어야만 한다 ${more} 보간은 message 인자에서 사용될수 있다

number.positive

value 는 반드시 positive number 이어야 한다.

number.negative

value 는 반드시 negative number 이어야 한다

number.integer

valueinteger 이어야 한다

number.round

valueMathmethod를 통해 실수값을 조정한다. (default 는 round 이다.)

date

Date Schema 를 정의한다.

let schema = yup.date()

await schema.isValid(new Date()) // => true

default 로 변경되는 cast 로직은 date 를 사용한다. 이는 Date 생성자를 통해 만들어지며, 이는 ISO date 문자열로 구문분석을 시도한다.

실패시 invalied Date 를 반환한다.

date.min

최소 date 값을 설정한다.

date.max

최대 date 값을 설정한다.

array

array 스키마를 정의한다.

let schema = yup.array().of(yup.number().min(2))

await schema.isValid([2, 3]) // => true
await schema.isValid([1, -24]) // => false

schema.cast(['2', '3']) // => [2, 3]

위는 array 의 값이 2 보다 크면 ture, 아니면 false 이다. subtype 스키마를 편의상 array 생성자를 거쳐 사용할수 있다

array().of(yup.number())
// or
array(yup.number())

Array기본 캐스팅 동작없이 작동한다.

array.of

array elementschema를 지정한다. of() 는 선택적이고, 제외하면 해당 contents 의 유효성을 검사하지 않는다.

array.json

input 된 값을, JSON.parse 을 사용하여 string 으로 구분분석한다.

array.length

Array 를 통해 특정 length 요구사항을 설정한다. ${length} 보간은 message 인자에서 사용할 수 있다.

array.min

Array 의 최소 length 값을 설정한다. ${min} 보간은 message 인자에서 사용할 수 있다.

array.max

Array 의 최대 length 값을 설정한다. ${max} 보간은 message 인자에서 사용할 수 있다.

array.ensure

값이 배열인지 보장한다. default[] 으로 설정하고, null 그리고 undefined 는 비어있는 arrya 로 변환한다.

비어있지 않고, 배열이 아닌 값은 arraywrapper 한다.

array().ensure().cast(null) // => []
array().ensure().cast(1) // => [1]
array().ensure().cast([1]) // => [1]

array.compact

falsy 값은 array 에서 제거한다. 또한, rejector 라는 함수를 제거하여, 제거할 value 를 설정할 수 도 있다.

array().compact().cast(['', 1, 0, 4, false, null]) // => [1, 4]

array()
  .compact(function (v) {
    return v == null
  })
  .cast(['', 1, 0, 4, false, null]) // => ['', 1, 0, 4, false]

tuple

tuplearraylength 를 고정한다. 각 항목에는 고유한 타입이 있다

import { tuple, string, number, InferType } from 'yup'

let schema = tuple([string().label('name'), number().label('age').positive().integer()])

await schema.validate(['James', 3]) // ['James', 3]

await schema.validate(['James', -24]) // => ValidationError: age must be a positive number

InferType<typeof schema> // [string, number] | undefined

tupledefault casting 이 없다.

object

object 스키마를 정의한다. isValid 안에 전달된 옵션들은 child schemas 에게도 역시 전달된다.

yup.object({
  name: string().required(),
  age: number().required().positive().integer(),
  email: string().email(),
  website: string().url(),
})

object schema default

기본적으로 object 내부에 fielddefaultset 되어 있다면, 기본값으로 설정되어 있다.

schema.default() 로 하면 해당 값이 이미 설정되어 있다.

const schema = object({
  name: string().default(''),
})

schema.default() // -> { name: '' }

이러한 방식은 큰 객체를 만들때, 매우 도움이될것이다. 하지만, 그렇지 않은 경우가 존재한다고 한다.

바로 requiredfield 는 실패할수 있기 때문이다. 그러므로, 이러한 부분을 잘 확인하고 적용해야 한다.

const schema = object({
  id: string().required(),
  names: object({
    first: string().required(),
  }),
})

schema.isValid({ id: 1 }) // false! names.first is required

object.shape

object key 와 해당 keys 에 대한 sechma 를 말한다.

다음을 보도록 하자.

object({
  a: string(),
  b: number(),
}).shape({
  b: string(),
  c: number(),
})

위의 예시는 마치 Objec.assign 처럼 작동한다. 이는 다음의 결과값을 반환한다.

object({
  a: string(),
  b: string(),
  c: number(),
})

object.json

JSON.parse 를 사용하여 JSON 객체를 구문분석하기 위해 시도한다.

object.concat

SchemaB 를 받아서, 새로운 Schema를 만든다. 이때, object shapeshallowly merg 된다.

즉, 같은 field 가 있을경우, schemaB 로 덮어씌어진다.

object.pick

하위집합에 있는 field 를 선택하여, 새로운 schema 를 만든하위집합에 있는 field 를 선택하여, 새로운 schema 를 만든다

const person = object({
  age: number().default(30).required(),
  name: string().default('pat').required(),
  color: string().default('red').required(),
})

const nameAndAge = person.pick(['name', 'age'])
nameAndAge.getDefault() // => { age: 30, name: 'pat'}

object.omit

하위집합에 있는 field 를 제거한 새로운 schema 를 만든다.

const person = object({
  age: number().default(30).required(),
  name: string().default('pat').required(),
  color: string().default('red').required(),
})

const nameAndAge = person.omit(['color'])
nameAndAge.getDefault() // => { age: 30, name: 'pat'}

object.from

지정한 key 를 새로운 key 로 변환한다. 만약, aliastrue 일때, old key 는 남아있는다.

let schema = object({
  myProp: mixed(),
  Other: mixed(),
})
  .from('prop', 'myProp')
  .from('other', 'Other', true)

schema.cast({ prop: 5, other: 6 }) // => { myProp: 5, other: 6, Other: 6 }

object.noUnknown

정의되지 않은 field 가 입력값에 존재하는 경우 해당 field 를 무시하고 유효성 검사를 수행한다.

onlyKnownKeys 매개변수는 검증 스키마가 정의되지 않은 필드가 입력값에 존재하는 경우에 대한 처리방식을 지정한다.

true 인 경우, 검증 스키마에 정의되지 않은 필드가 입력값에 존재하면 해당 필드를 무시하고 유효성 검사를 수행한다.

flase 인 경우, 검증 스키마에 정의되지 않은 필드가 입력값에 존재하면 유효성 검사를 실패한다.

message 매개변수는 실패시 반환하는 error 메시지이다.

object.noUnknown() 메서드는 object.shape() 와 함께 사용하여 객체의 유효성 검사를 수행할 때 유용하게 사용된다.

import * as yup from 'yup'

const schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required(),
})

const data = {
  name: 'John Doe',
  age: 30,
  address: '123 Main St.', // 검증 스키마에 없는 필드
}

schema
  .noUnknown()
  .validate(data)
  .then((valid) => console.log(valid))
  .catch((err) => console.error(err))

object.camelCase

keys 들을 camelCase 로 변환시킨다.

object.constantCase

keys 를 전부 대분자 스네이크케이스(CONSTNAT_CASE) 로 변환한다.