[React]zodに出会って小さな幸せを得た話
概要
zodの使い道について、react-hook-formと組み合わせたフォームバリデーションが便利!というくらいの認識でしたが、他にも便利な使い方があったので、まとめます。
as string
による型アサーションの罪悪感からの解放
string | undefined
になる問題を解決できた話
①環境変数が例えば以下のような環境変数が定義されていたとします。
NEXT_PUBLIC_BASE_URL=http://localhost:3000
そしてNEXT_PUBLIC_BASE_URL
を使う際に必ずundifned
の考慮が必要になります。
これは、環境変数の値が実際に存在するかどうかは、ランタイムでしか判定できず、コンパイル時には undefined
の可能性があるからです。
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
ただ、環境変数が定義されていない(undefined
になる)のは、開発者の設定もれなどの人為的ミスであることが多いため、設定されていることを前提にas string
でアサーションするケースが多いと思います。
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL as string;
このようなケースにzodが活用できます。
以下の.env
を例にします。
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_SITE_NAME=HOGEHOGE
NEXT_PUBLIC_OBJECT={"id":"1","name":"hoge"}
NEXT_PUBLIC_NUMBER=5
API_KEY=fdfahsjfhasfuasdhfabfasjfhahfeuryq854789350qj
API_SECRET_KEY=fjakfdas45893ohjfhsay8r43dfasjfh
まず、clientとserverで使用する環境変数をそれぞれschema定義してみましょう。
- server
export const envSchema = z.object({
API_KEY: z.string(),
API_SECRET_KEY: z.string(),
});
export type EnvType = z.infer<typeof envSchema>;
z.object
でオブジェクトの各プロパティの期待される型を定義します。各、環境変数を一つのオブジェクトとしてschema定義しています。
作成したshemaからz.infer<typeof envSchema>;
を使ってtypescriptの型を推論しています。
つまり
type EnvType = z.infer<typeof envSchema>;
は
type EnvType = {
API_KEY: string;
API_SECRET_KEY: string;
};
と同様になります。
- client
export const envSchema = z.object({
NEXT_PUBLIC_BASE_URL: z.string().url(),
NEXT_PUBLIC_SITE_NAME: z.string(),
NEXT_PUBLIC_OBJECT: z.string().transform((val) => {
try {
return z
.object({
id: z.string(),
name: z.string(),
})
.parse(JSON.parse(val));
} catch (e) {
throw new Error('NEXT_PUBLIC_OBJECTのparseに失敗しました');
}
}),
NEXT_PUBLIC_NUMBER: z.string().transform((val) => parseInt(val, 10)),
});
export type EnvType = z.infer<typeof envSchema>;
以下のようにobject
とnumber
のような型はどうでしょう?
NEXT_PUBLIC_OBJECT={"id":"1","name":"hoge"}
NEXT_PUBLIC_NUMBER=5
環境変数は全てstring
型と扱われるため
z.string().transform
で型を変換することができます。
取得例
import {envShema, type EnvType} from "./publicSchema"
let publicEnv: EnvType;
try {
publicEnv = envSchema.parse({
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME,
NEXT_PUBLIC_OBJECT: process.env.NEXT_PUBLIC_OBJECT,
NEXT_PUBLIC_NUMBER: process.env.NEXT_PUBLIC_NUMBER,
});
} catch (error) {
console.error(error);
throw new Error(`Invalid public env variables!`);
}
export default publicEnv;
import {envShema, type EnvType} from "./serverSchema"
let serverEnv: EnvType;
try {
serverEnv = envSchema.parse({
API_KEY: process.env.API_KEY,
API_SECRET_KEY: process.env.API_SECRET_KEY,
});
} catch (error) {
console.error(error);
throw new Error(`Invalid server env variables!`);
}
export default serverEnv;
publicEnv = envSchema.parse({
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME,
NEXT_PUBLIC_OBJECT: process.env.NEXT_PUBLIC_OBJECT,
NEXT_PUBLIC_NUMBER: process.env.NEXT_PUBLIC_NUMBER,
});
で定義したスキーマに基づいて入力データを検証しています。このメソッドは、与えられた入力がスキーマに合致するかどうかをチェックし、合致する場合は入力をそのまま返し、合致しない場合はエラーを投げます。
import publicEnv from "getPublicConfig"
publicEnv.NEXT_PUBLIC_BASE_URL
これによりas string
の罪悪感からの解放と環境変数をより型安全に使うことが可能になりました。
string[] | string | undefined
になる問題を解決できた話
②queryパラメータが例えば、GetServerSideProps
などでqueryパラメータを取得して何か処理を行いたいケースはよくあることでしょう。
その場合、取得したパラメータの値はstring[] | string | undefined
になります。
パラメータでpageNumberとkeywordを受け取るページを想定してみます。
export const getServerSideProps: GetServerSideProps = async (context) => {
const { query } = context;
const keyword = query.keyword;
const pageNumber = query.page;
どうしてもstring[]
が邪魔になり、as string
に手を染めてしまうことが多々あります。
そもそも、なぜstring[]
が想定されてしまうのかというと、同じqueryパラメータが2つ以上ある場合、配列にまとめられてしまうからです。
下記のURLのことを指します。
http://localhost:3000/test-page?page=1&page=2&page=3
page
というパラメータが3つあります。この場合
const pageNumber = query.page;
の値が
[1, 2, 3]
となるからです。
しかし多くの場合は、同じパラメータは許容したくなく、string | undefined
となるのが理想でしょう。
こういったケースにもzodを使うことができます。
- まず、queryパラメータをバリデートするutility関数を作成します。
import { ZodType } from "zod";
export default function validateQueryParam(
query: string[] | string | undefined,
schema: ZodType,
): ZodType | null {
if (!query) return null;
try {
// 配列の場合は最初の値を使用
if (Array.isArray(query)) query = query[0];
return schema.parse(query);
} catch (error) {
console.error(error);
throw new Error("Invalid query param");
}
}
// 配列の場合は最初の値を使用
if (Array.isArray(query)) query = query[0];
もし配列が来た場合は、最初のindexを使用するようにします。
そして第二引数のzodのschemaでqueryをparseして検証した結果を返します。
- GetServerSidePropsでは、zodのschemaを渡すことでパラメータの検証と型の変換を同時に行うことができます。
export const getServerSideProps: GetServerSideProps = async (context) => {
const { query } = context;
const keyword = validateQueryParam(query.keyword, z.string().optional());
// number型でパラメータを取得できる
const pageNumber = validateQueryParam(
query.page,
z
.string()
.optional()
.transform((val) => {
const res = parseInt(val!, 10);
if (isNaN(res)) {
throw new Error("invalid query");
}
}),
);
return {
props: {
pageNumber,
keyword,
},
};
};
こうすることで、汎用的にhttp://localhost:3000/test-page?page=あいうえお
などのパラメータを弾くことが可能になります。
今回は二点ほど便利だと感じた使い方を紹介しましたが、APIルートでの型の検証など使い所はたくさんありそうだと感じました。
Discussion
いいね