z.enum(), z.optional() 구현하기
- #1: z.string() 구현하기
- #3: z.string() min, max 메서드 구현하기
- #3: z.enum(), z.optional() 구현하기
- #4: z.object() 구현하기
이전 글에서는 간단한 string
타입을 검증할 수 있는 ZodString
클래스를 구현해 봤습니다.
오늘은 enum
타입을 검증할 수 있는 ZodEnum
과
선택적으로 인풋을 받기 위해 사용하는 ZodOptional
을 구현해 보겠습니다.
ZodEnum
Zod enum을 사용하면 enum
스키마를 생성할 수 있다.
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
type FishEnum = z.infer<typeof FishEnum>;
// "Salmon" | "Tuna" | "Trout"
입력받은 data가 FishEnum
타입과 다르다면 다음과 같이 검증에 실패한다.
const result1 = FishEnum.safeParse("Salmon");
// { success: true, data: "Salmon" }
const result2 = FishEnum.safeParse("Salmon2");
// { success: false, error: Error }
구현해보기
이제 어떻게 사용하는지 파악했으므로 간단한 버전으로 구현해 보자.
// 2. values는(ex. ["Salmon", "Tuna", "Trout"]) 하나 이상의 string으로
// 이루어진 배열이므로 타입 파라미터 T를 다음과 같이 선언할 수 있다.
// 또한 추론되어야 하는 타입은 배열의 멤버이므로 ZodType의 파라미터에 T[number]를 인자로 전달해 준다.
class ZodEnum<T extends [string, ...string[]]> extends ZodType<T[number]> {
// 1. 허용된 enum 값을 저장하는 변수
readonly values: T;
constructor(values: T) {
super();
this.values = values;
}
_parse(
data: unknown
):
| { isValid: false; reason?: string | undefined }
| { isValid: true; data: T[number] } {
// 3. data가 string이 아니라면 더 이상 확인하지 않는다.
if (typeof data !== "string") {
return {
isValid: false,
};
}
// 4. values에 data가 없다면 유효하지 않다.
if (!this.values.includes(data)) {
return {
isValid: false,
};
}
// 5. 위에서 걸러지지 않았다면 유효하다.
return {
isValid: true,
data,
};
}
static create<T extends [string, ...string[]]>(values: T) {
return new ZodEnum(values);
}
}
이렇게 구현을 하면 검증 로직이 잘 동작하는 것을 확인할 수 있다.
const FishEnum = ZodEnum.create(["Salmon", "Tuna", "Trout"]);
FishEnum.safeParse("Trout"); // 통과
FishEnum.safeParse("Trout!!"); // 실패
하지만 지난 글에서 만든 Infer
로 타입을 추출해 보면
string
으로 잘못 추론되고 있다. 우리가 원하는 타입 "Salmon" | "Tuna" | "Trout"
과는 다르다.
type FishEnum = Infer<typeof FishEnum>;
// ^? string
이를 개선하기 위해 create
메서드의 타입을 변경해 보자. 현재는 단순히 string
으로 이루어진 배열을 확장하는 타입으로 타입 파라미터가 선언되어 있기 때문에
타입스크립트가 멤버의 타입을 string
으로 추론해 주었다.
static create<T extends [string, ...string[]]>(values: T) {
return new ZodEnum(values);
}
하지만 우리는 string
보다 더 구체적인 타입으로 추론해 주길 원하기 때문에 다음과 같은 트릭을 사용할 수 있다.
string
타입을 확장하는 새로운 타입 파라미터 U
를 선언해 주고 기존의 T
는 U
로 이루어진 배열을 확장하도록 변경해 준다.
static create<U extends string, T extends [U, ...U[]]>(values: T) {
return new ZodEnum(values);
}
그러면 타입스크립트가 배열의 멤버를 string
타입보다 조금 더 구체적인 타입으로 추론해 주기 때문에 원하는 결과를 얻을 수 있다.
type FishEnum = Infer<typeof FishEnum>;
// ^? "Salmon" | "Tuna" | "Trout"
ZodOptional
스키마를 선언하다 보면 있을 수도, 없을 수도 있는 필드를 검증하고 싶을 때가 있다.
예를 들면 사용자에게 주소를 선택적으로 입력하도록 만들고 해당 데이터를 검증한다고 하자.
const optionalAddress = z.string().optional(); // string | undefined
// 다음과 같이 선언할 수도 있다.
const optionalAddress = z.optional(z.string()); // string | undefined
그러면 z.string()
으로 스키마를 선언했을 때와는 다르게 데이터가 입력되지 않았더라도(undefined
여도)
검증이 실패하지 않는다.
optionalAddress.safeParse("서울특별시..."); // 통과
optionalAddress.safeParse(undefined); // 통과
구현 해보기
// 2. innerType에는 어떠한 값이 들어올지 모르기 때문에 타입 파라미터 T를 선언하고
// 이는 ZodType을 확장하는 값만 허용하도록 해준다.
// 또한 해당 스키마의 타입을 추출했을 때 ZodString 타입이 입력되었다면 string | undefined로
// 추론되어야 하므로 ZodType에 T["_output"] | undefined을 인자로 전달해 준다.
class ZodOptional<T extends ZodType<any>> extends ZodType<
T["_output"] | undefined
> {
// 1. 기존에 어떠한 타입인지를 저장하는 변수
// ZodString, ZodEnum 등 ZodType을 확장한다면 모두 가능하다.
readonly innerType: T;
constructor(innerType: T) {
super();
this.innerType = innerType;
}
_parse(
data: unknown
):
| { isValid: false; reason?: string | undefined }
| { isValid: true; data: T | undefined } {
// 3. undefined이면 더이상 확인할 필요 없이 통과
if (typeof data === "undefined") {
return {
isValid: true,
data,
};
}
// 4. undefined가 아니라면 innerType에게 역할을 넘김
return this.innerType._parse(data);
}
static create<T extends ZodType<any>>(innerType: T) {
return new ZodOptional(innerType);
}
}
이러면 주어진 타입을 선택적으로 입력받도록 만들 수 있고
const optionalAddress = ZodOptional.create(ZodString.create());
optionalAddress.safeParse("서울특별시..."); // 통과
optionalAddress.safeParse(undefined); // 통과
const optionalFish = ZodOptional.create(
ZodEnum.create(["Salmon", "Tuna", "Trout"])
);
optionalFish.safeParse("Salmon");
optionalFish.safeParse(undefined); // 통과
타입 추출 또한 잘 되게 만들 수 있다.
Infer<typeof optionalAddress>
// ^? string | undefined
Infer<typeof optionalFish>
// ^? "Salmon" | "Tuna" | "Trout" | undefined
주어진 타입을 선택적으로 받도록 만드는 기능은 모든 타입에서 공통적으로 사용할 수 있기 때문에 ZodType
의 메서드에 추가하면
더 편하게 사용 가능하다.
abstract class ZodType<Output> {
// 생략
optional() {
return ZodOptional.create(this);
}
}
const optionalAddress = ZodString.create().optional();
const optionalFish = ZodEnum.create(["Salmon", "Tuna", "Trout"]).optional();
비록 매우 단순화시킨 코드지만 지금까지 코드를 이해한다면 ZodNumber
, ZodArray
, ZodNullable
등도
어떻게 구현되어 있을지 예상할 수 있을 것이다. 다음 글에서는 마지막으로 ZodObject
를 어떻게 구현하는지 알아보자.