【TS】satisfiesじゃないとできないこと。~無駄な変数を撲滅する~。
こんにちは!
satisfies
大好きな筆者です。
この記事に引き続いて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
節に到達する前に、cat
・dog
・pig
すべてのcase
について網羅し、return
できているため、const _:never = animal
の部分に到達する時にはanimal
はnever
型となっています。すべての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) // !?
});
zod
のz.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
です。
まだあまり使ってない方は是非使ってみてくださいー!!
Discussion