🧩

[React]zodに出会って小さな幸せを得た話

2023/11/05に公開
1

概要

zodの使い道について、react-hook-formと組み合わせたフォームバリデーションが便利!というくらいの認識でしたが、他にも便利な使い方があったので、まとめます。

as stringによる型アサーションの罪悪感からの解放

①環境変数がstring | undefinedになる問題を解決できた話

例えば以下のような環境変数が定義されていたとします。

.env
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を例にします。

.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
serverSchema.ts
export const envSchema = z.object({
  API_KEY: z.string(),
  API_SECRET_KEY: z.string(),
});
export type EnvType = z.infer<typeof envSchema>;

z.objectでオブジェクトの各プロパティの期待される型を定義します。各、環境変数を一つのオブジェクトとしてschema定義しています。
https://zod.dev/?id=objects
作成したshemaからz.infer<typeof envSchema>;を使ってtypescriptの型を推論しています。
つまり

type EnvType = z.infer<typeof envSchema>;

type EnvType = {
  API_KEY: string;
  API_SECRET_KEY: string;
};

と同様になります。

  • client
publicSchema.ts
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>;

以下のようにobjectnumberのような型はどうでしょう?

NEXT_PUBLIC_OBJECT={"id":"1","name":"hoge"}
NEXT_PUBLIC_NUMBER=5

環境変数は全てstring型と扱われるため
z.string().transformで型を変換することができます。
https://zod.dev/?id=transform

取得例

getPubliConfig.ts
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;
getServerConfig.ts
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,
});

で定義したスキーマに基づいて入力データを検証しています。このメソッドは、与えられた入力がスキーマに合致するかどうかをチェックし、合致する場合は入力をそのまま返し、合致しない場合はエラーを投げます。
https://zod.dev/?id=parse


import publicEnv from "getPublicConfig"

publicEnv.NEXT_PUBLIC_BASE_URL

これによりas stringの罪悪感からの解放と環境変数をより型安全に使うことが可能になりました。

②queryパラメータがstring[] | string | undefinedになる問題を解決できた話

例えば、GetServerSidePropsなどでqueryパラメータを取得して何か処理を行いたいケースはよくあることでしょう。
その場合、取得したパラメータの値はstring[] | string | undefinedになります。

パラメータでpageNumberkeywordを受け取るページを想定してみます。

test-page.tsx
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関数を作成します。
src/util/validateQueryParam.ts
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ルートでの型の検証など使い所はたくさんありそうだと感じました。

参考
zod公式
Next.js の Zod 活用術

Discussion

みっきーみっきー

これによりas stringの罪悪感からの解放と環境変数をより型安全に使うことが可能になりました。

いいね