Gemcook Tech Blog
😆

【TS】satisfiesじゃないとできないこと。~無駄な変数を撲滅する~。

2024/08/20に公開

こんにちは!
satisfies大好きな筆者です。

https://zenn.dev/gemcook/articles/c1916975cf6ce6

この記事に引き続いてsatisfiesにしかできないことをもう一つ紹介したいと思います。satisfiesのお気に入りの使い方その2。です!

無駄な変数宣言せずに型をつける

型注釈は変数の宣言時に行うことでその変数に対し型をつけることができます。その性質上、どうしても変数は宣言する必要があります、しかしsatisfiesはそんなことありません。いつだって使えるのです。
さて「変数宣言せずに型をつける」とはどういうことでしょうか。実際の使用例を見ていきましょう。

例1: 網羅性チェックに...。

switchなどで対象の変数が取りうるすべての型について網羅的にチェックできているか?caseに漏れがないかをチェックするテクニックとして、neverを用いた網羅チェックがあります。

サンプルコードは以下の通りです。

type Animal = "dog" | "cat" | "pig";

const awesomeFunction = (animal: Animal) => {
  switch (animal) {
    case "cat":
      return "meow";
    case "dog":
      return "bowwow";
    case "pig":
      return "oink oink";
    default:
      const _: never = animal;
  }
};

default節に到達する前に、catdogpigすべてのcaseについて網羅し、returnできているため、const _:never = animalの部分に到達する時にはanimalnever型となっています。すべてのcaseが網羅できているため型エラーが出ません。

もしも、Animalに新しくcowなどが追加された場合、case "cow":のパターンが網羅できていないため、const _:never = animal部分でコンパイルエラーとなってswitch文の修正が必要であることが検出できるわけです。

筆者は、割とこのテクニックが好きで愛用しているのですが、「なんか嫌だな」と思っている部分もありました。それが、まさにタイトルにもある通り、「無駄な変数宣言してるな...。」です。
この問題を解決してくれるのがsatisfiesです。

satisfiesで無駄な変数宣言撲滅

satisfiesを使用したパターンでの実装を見てみましょう。

type Animal = "dog" | "cat" | "pig";

const awesomeFunction = (animal: Animal) => {
  switch (animal) {
    case "cat":
      return "meow";
    case "dog":
      return "bowwow";
    case "pig":
      return "oink oink";
    default:
      return animal satisfies never;
  }
};

いいですねー!!無駄な変数(_)がなくなりました!!
網羅チェックで必要なことは「default説まで生き残ったanimalがいるかどうか?をチェックする」ことだけです。変な変数の宣言は本来必要ないはずです。
これで無駄な変数宣言をなくし、必要十分な実装になりましたね😸

例2: 関数の引数に...。

ライブラリなどで、「引数にオブジェクトを受け取るが、keyはstringであればなんでも良い。」という関数によく出会います。そんな時にもsatisfiesが活躍します。
具体例にzodを使用して、下記画像のような、「名前」と「住所」を入力するだけの、簡単(雑)なフォームを実装することを考えたいと思います。

satisfiesのない世界でのzodスキーマの宣言

上記の"簡単なフォーム"を実装するとしたら以下のようになるでしょう。(本題に関係ないコードは色々省略しています & react-hook-formを使用しています)

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

const awesomeFormSchema = z.object({
  name: z.string(),
  address: z.string(),
});

type AwesomeFormSchema = z.infer<typeof awesomeFormSchema>;

const AwesomeFormPage = () => {
  const { register } = useForm<AwesomeFormSchema>({
    resolver: zodResolver(awesomeFormSchema),
  });

  return (
    <form>
      <label>
        名前
        <input {...register("name")} />
      </label>
      <label>
        住所
        <input {...register("address")} />
      </label>
      {/* submitボタンは割愛...。 */}
    </form>
  );
};

なんだか寂しいので、
2つのinputはそれぞれ必須入力として、バリデーションエラーのメッセージも出るようにしましょう。

コードは以下のようになりました。

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

const awesomeFormSchema = z.object({
+  name: z.string().min(1, {message: "名前は必須項目です。"}),
+  address: z.string().min(1, {message: "住所は必須項目です。"}),
});

type AwesomeFormSchema = z.infer<typeof awesomeFormSchema>;

const AwesomeFormPage = () => {
  const {
    register,
+   formState: { errors },
  } = useForm<AwesomeFormSchema>({
    resolver: zodResolver(awesomeFormSchema),
  });

  return (
    <form>
      <label>
        名前
        <input {...register("name")} />
+       <p>{errors.name?.message}</p>
      </label>
      <label>
        住所
        <input {...register("address")} />
+       <p>{errors.address?.message}</p>
      </label>
      {/* submitボタンは割愛...。 */}
    </form>
  );
};

一応完成しました...が、「バリデーションメッセージの”名前”」と「ラベルの"名前"」がそれぞれ別々にハードコードされているのが気に入りません...。それらの共通化をしていきたいと思います。

const awesomeFormLabel = {
  name: "名前",
  address: "住所",
};

const awesomeFormSchema = z.object({
  name: z.string().min(1, { message: `${awesomeFormLabel.name}は必須項目です。` }),
  address: z.string().min(1, { message: `${awesomeFormLabel.address}は必須項目です。` }),
});

type AwesomeFormSchema = z.infer<typeof awesomeFormSchema>;

const AwesomeFormPage = () => {
  const {
    register,
    formState: { errors },
  } = useForm<AwesomeFormSchema>({
    resolver: zodResolver(awesomeFormSchema),
  });

  return (
    <form>
      <label>
        {awesomeFormLabel.name}
        <input {...register("name")} />
        <p>{errors.name?.message}</p>
      </label>
      <label>
        {awesomeFormLabel.address}
        <input {...register("address")} />
        <p>{errors.address?.message}</p>
      </label>
      {/* submitボタンは割愛...。 */}
    </form>
  );
};

のようにしてみました。
しかし、awesomeFormLabelと、awesomeFormSchemaに型的な繋がりがないため、自由にばらばらなkeyで値を設定できてしまいます。(気持ち悪くて天国に行けそうです👼)

const awesomeFormLabel = {
  name: "名前",
  address: "住所",
  hoge: "らせん階段", // !?
  fuga: "カブトムシ", // !?
  fizz: "廃墟の街", // !?
  ...
};

const awesomeFormSchema = z.object({
  name: z.string().min(1, { message: `${awesomeFormLabel.name}は必須項目です。` }),
  address: z.string().min(1, { message: `${awesomeFormLabel.address}は必須項目です。` }),
  wards: z.array(z.string()).length(14) // !?
});

zodz.object()は前述の「引数にオブジェクトを受け取るが、keyはstringであればなんでも良い。」という関数であり、自由なkeyにそれぞれバリデーションを指定することになります。
それぞれを型的に対応させようとすると、以下のようにz.object()に渡すオブジェクトを変数として切り出す必要があります。

import {z, type ZodSchema } from "zod"

type SchemaKey = "name" | "address"

const awesomeFormLabel: Record<SchemaKey, string> = {
  name: "名前",
  address: "住所",
}

// 型注釈により型をつけておく。
const aweSomeFormSchemaObject: Record<SchemaKey, ZodSchema> = {
  name: z.string().min(1, { message: `${awesomeFormLabel.name}は必須項目です。`}),
  address: z.string().min(1, { message: `${awesomeFormLabel.address}は必須項目です。` }),
}

const awesomeFormSchema = z.object(aweSomeFormSchemaObject),

例1でも言いましたが、筆者はこれが好きではありません。今回もaweSomeFormSchemaObjectに対して「無駄な変数宣言してるな...。」と感じます。
ではどうすればいいのか? そうですね、satisfiesです!

satisfiesで無駄な変数宣言撲滅

さて、前置きが長くなりましたが、以下のようにsatisfiesを使用することで、前述の不要な変数はなしに、やりたいことを実現することができます。

import {z, type ZodSchema } from "zod"

type SchemaKey = "name" | "address"

const awesomeFormLabel= {
  name: "名前",
  address: "住所",
} as const satisfies Record<SchemaKey, string> 

const awesomeFormSchema = z.object({
  name: z.string().min(1, { message: `${awesomeFormLabel.name}は必須項目です。`}),
  address: z.string().min(1, { message: `${awesomeFormLabel.address}は必須項目です。` }),
} satisfies Record<SchemaKey, ZodSchema>),

無駄な変数が消え、スッキリしつつもawesomeFormLabelと、awesomeFormSchemaの間に型的な繋がりを作ることができました。

z.object()の引数として渡すオブジェクトに一時的な型をつけています。例1でも同様ですがsatisfiesを使用する際は必ずしもconstなどで変数を宣言する必要はないということですね。

このように、satisfiesは変数宣言時その末尾につけることで「推論による具体的な型を保ったまま一時的な型をつける」だけでなく、上記の様に「別に変数に切り出したくないけど、型で縛りたい...。」といったときに使用することで、無駄な変数宣言を削減できることがわかっていただけたかと思います。

...便利ですね!好き!

まとめ

今回は、satisfiesの利用によって、不要な変数宣言の削減が可能なことを紹介しました。
例にあげた以外にも色々と活躍の場があるsatisfiesです。
まだあまり使ってない方は是非使ってみてくださいー!!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion