バリデーションとTypeScript の見かけ上の型
(注意: このページは中心部分は完成してるけど、 Wrapper (導入とか) がまだ不足してるよ!)
TypeScript ではなぜ不十分か
TypeScript は、一見完全に型がついてるように見えるよね。
const num: number = 10 as any; // "10" as any に置き換えてみてnum.toFixed(2); // -> '10.00'
でも、この型はコンパイル時のみに存在するもので、実行時には完全に消えてるよ。
bunx tsc ./index.ts --outFile ./out.js
const num = 10;num.toFixed(2);
これは、 Bun や Deno などの TypeScript を直接実行できると謳っているランタイムでも同じ。
バリデーションをしないと、例えばパスワード認証スキップとかの荒業ができるよ。 (複雑なので詳細は発展演習に回すね)
なので、バリデーションは必要!
(NOTE from devs: 導入は適当だけど、必要なのは正しいよ。)
バリデーションをしてみる
では、実際に TypeScript でスキーマを作り、既存のアプリケーションにバリデーションを追加してみよう。
もととなるアプリケーションはこれ。
ソースコードが欲しい場合は、このドキュメントのリポジトリをクローンして、アプリのあるディレクトリ exercises/t1-validation.install/
を開いてね。
const app = new Hono();
app.patch("/users/:id", async (c) => { const id = Number.parseInt(c.req.param().id); type Body = { password: string; data: { name?: string; password?: string }; } const body = (await c.req.json()) as Body;
const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201);});
import * as v from "valibot";
const RequestSchema = v.object({ password: v.string(), data: v.object({ name: v.optional(v.string()), password: v.optional(v.string()), }),});app.patch("/users/:id", async (c) => { const id = v.parse(v.number(), Number.parseInt(c.req.param().id)); const body = v.parse(RequestSchema, await c.req.json());
const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201);});
import { z } from "zod";
const RequestSchema = z.object({ password: z.string(), data: z.object({ name: z.string().optional(), password: z.string().optional(), }),});app.patch("/users/:id", async (c) => { const id = z.number().parse(Number.parseInt(c.req.param().id)); const body = RequestSchema.parse(await c.req.json());
const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201);});
Typia では型がスキーマの役目を果たすので、すでにある型をそのまま流用できる。
代わりに、環境構築が少し面倒くさい。 公式ドキュメントを参考のこと。
import typia from "typia";
app.patch("/users/:id", async (c) => { const id = typia.assert<number>(Number.parseInt(c.req.param().id)); type Body = { password: string; data: { name?: string; password?: string }; }; const body = typia.assert<Body>(await c.req.json()); const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201);});
(以下蛇足) Hono のヘルパー関数
ちなみに、Hono だともっと便利なバリデーターが提供されてるよ。
import { vValidator } from "@hono/valibot-validator";
app.patch( "/users/:id", vValidator( "param", v.object({ id: v.pipe(v.string(), v.transform(Number.parseInt)) }), ), vValidator( "json", v.object({ password: v.string(), data: v.object({ name: v.optional(v.string()), password: v.optional(v.string()), }), }), ), async (c) => { const { id } = c.req.valid("param"); const body = c.req.valid("json");
const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201); },);
import { zValidator } from "@hono/zod-validator";
app.patch( "/users/:id", zValidator("param", z.object({ id: z.coerce.number() })), zValidator( "json", z.object({ password: z.string(), data: z.object({ name: z.string().optional(), password: z.string().optional(), }), }), ), async (c) => { const { id } = c.req.valid("param"); const body = c.req.valid("json");
const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201); },);
import { typiaValidator } from "@hono/typia-validator";
app.patch( "/users/:id", typiaValidator( "json", typia.createValidate<{ password: string; data: { name?: string; password?: string }; }>(), ), async (c) => { const id = typia.assert<number>(Number.parseInt(c.req.param().id)); const body = c.req.valid("json");
const res = await prisma.user.update({ where: { id, password: body.password }, data: body.data, }); return c.json(res, 201); },);
演習問題 (発展)
バリデーションのない脆弱なアプリケーション exercises/t1-validation.hack-this
をハックしてみよう。
要件は次の2つ。
server/
の中は変更しないbun server
で起動したアプリケーションに対して、パスワードを知らずに、ユーザーの名前を勝手に変える
詳しくはそのディレクトリにある README を見てね。