TypeScriptのゾッとする話 ~ Zodの紹介 ~
この記事について
先日、「 エンジニアとして一番怖いモノは何? 」と知人に尋ねると、
「 実は、TypeScript が一番怖い 」
と言ってきました。
そんな訳ないだろうと思っていたのですが、どうやら知人は本気の様子。
「 TypeScript が嫌いならまだしも、TypeScript が怖いとは、これはナニかあるな 🤔 」
と思った私は、TypeScript First なライブラリである Zod を知人に紹介して、事の真意を確かめようとしたのでした。 怖いならもっと怖がらせてやろうと思ったのは内緒 🤫
しかし、知人に紹介するだけでは勿体ないので、今回は皆さんに知見を交えながら Zod の事を紹介していこうと思います 💪
注意として、この記事は以下のリポジトリの README.md を参考にしています。
そのため、記事内のソースコードには README.md 内のソースコードを引用している部分がありますので、ご理解いただけますと幸いです 🙏
では早速、解説してきましょー 👨🚀👩🚀
Zod について
Zodとは、TypeScript First なバリデーションライブラリで、Blitzにも使われている今 Hot なライブラリです。
特徴としては、以下のようなものがあります 👇
特徴
- 依存関係ゼロ
- ブラウザと Node.js で動作します( deno にも対応 🦕 )
- サイズが小さい: 8kb minified + zipped
- イミュータブル
- 簡潔で連鎖可能なインターフェース
- Functional approach: parse, don't validate
- プレーン JavaScript でも動作します!TypeScript を使用しなくても大丈夫!
基本的な使い方
先ずは基本的な使い方から見て行きましょう 👀
import * as z from "zod";
// スキーマを作成
const schema = z.object({
str: z.string(),
})
// 値を検証する
try {
const ok = schema.parse({ str: "" });
const throw_error = schema.parse({ str: 0 });
} catch(err) { // 検証に失敗するとエラーが投げられます。
console.error(err);
/*
// エラーオブジェクトの内容👇
[
{
"code": "invalid_type", // エラータイプ
"expected": "string", // 期待した型
"received": "number", // 受け取った値の型
"path": [ "str" ], // エラーが発生したプロパティへのパス
"message": "Expected string, received number" // エラー内容
}
]
*/
}
上記のコードでは、{ str: string }
の値を期待するschema
を定義し、そのschema
が持つparse()
を実行する事で、引数に渡された値が期待する型かどうかを検証しています。
この時、schema.parse()
の動作は以下のようになります。
- ✅ 検証成功時 -> 期待した型の値を返します
- ❌ 検証失敗時 -> 検証エラーの内容を持った ZodError オブジェクトを投げます
.parse()
は、検証失敗時には Error オブジェクトを投げるため try...catch 文 を使う必要がありますが、Error を投げない検証方法もありますので、状況によって使い分けることが可能です。
また注意点としては、検証値がプリミティブ型ではない時、検証結果は引数の値とは違う値になります。そのため、以下の処理は常にfalse
を返すことになります 👇
const value = { str: "test" }
const value2 = schema.parse(value) // 検証が成功し、結果をsameValueに入れる
// 引数の値を返すわけではありませんので、以下の処理はfalseになります🙅♂️🙅♀️
console.log( value === value2 ); // ==> false
上記の挙動が特に問題となる事は少ないと思いますが、知見として共有しておきます 🦴
型の生成
次に最大の特徴である、生成したスキーマから TypeScript の型を生成する機能を紹介したいと思います 🐰
import * as z from "zod";
const schema = z.object({
str: z.string(),
num: z.number()
})
// スキーマから型を生成!
type SchemaType = z.infer<typeof schema>;
/*
{
str: string
num: string
}
*/
上記のソースコード中にあるz.infer<>
の型引数に、スキーマの型( typeof で変換した型 )を渡すことで、スキーマが期待している値の型を生成することが出来ます。
この機能が凄い所は、今まで型定義をしてから実装する所を、ほとんど実装のみの記述で済む点です。これによって、型定義と実装との齟齬を無くす事が出来るうえに記述量を減らすことが出来ます。
勿論、型生成できるモノは string 型や number 型だけでなく他の型も生成可能です。
それらの型の詳細は後述しますが、ほとんどの型を生成できるので、相当特殊な要件ではない限り型を生成できると思います。
まとめ
さて、これで基本的な使い方の解説は終了です。
少し短い解説に思えるかもしれませんが、基本的にはスキーマ作って検証するだけなので、上記の使い方を覚えておけば Zod を扱えるようになります。何なら、VSCode の入力補完を活用すれば、ドキュメントを読まずとも書けてしまうくらいです。そのくらいシンプルな API 設計も Zod の魅力ですね ✨
次は、スキーマを定義するための関数(以後、ZodType)について解説していきます 🌭
String 型
String 型のスキーマを定義するにはz.string()
を使って定義します。
String 型のスキーマには様々なオプションが用意されているので、そのオプションを使う事で柔軟な定義が可能となっています 👇
import * as z from "zod";
z.string(); // 単純な文字列
z.string().min(5); // 5文字以上の文字列
z.string().max(5); // 5文字以下の文字列
z.string().length(5); // 固定幅の文字列
z.string().email(); // メールアドレス文字列
z.string().url(); // URL文字列
z.string().uuid(); // UUID文字列
z.string().regex(regex); // 正規表現にマッチする文字列
z.string().nonempty(); // 空文字列以外の文字列
上記の email・url・uuid
などのオプションは、validator.jsを用いて検証しますので、扱う際はvalidator.jsを参照してください。
Number 型
Number 型のスキーマを定義するにはz.number()
を使って定義します。
Number 型にも以下の便利なオプションが用意されています 👇
import * as z from "zod";
z.number(); // 単純な数値( NaNとBigInt型は含まない )
z.number().min(5); // 5以上の数値( >= 5 )
z.number().max(5); // 5以下の数値( <= 5 )
z.number().int(); // 整数型の数値
z.number().positive(); // 0よりも大きい数値( > 0 )
z.number().nonnegative(); // 0以上の数値( >= 0 )
z.number().negative(); // 0より小さい数値( < 0 )
z.number().nonpositive(); // 0以下の数値( <= 0 )
注意点としては、NaN と BigInt 型を含まない事ぐらいでしょうか。
BigInt 型は別の ZodType として用意されているので大丈夫かと思いますが、NaN
については ZodType が用意されていないので、自作する必要があります 👇
// NaNを検知するスキーマ( ZodSchemaを用いてnumber型にする )
const NaNSchema: z.ZodSchema<number> = z.any().refine(Number.isNaN)
// NaNを許容する数値スキーマ
const NumOrNaNSchema = z.number().or(NaNSchema)
// 型生成( NaNSchemaをZodSchemaでnumber型にしてないとanyになるので注意! )
type SchemaType = z.infer<typeof NumOrNaNSchema>;
// number
NaN を許容する時なんてあるのかは甚だ疑問ですが、IT 業界は稀がよくある業界なので一応共有しておきます 🎫
Boolean 型
Boolean 型のスキーマを定義するにはz.boolean()
を使って定義します。
Boolean 型には固有のオプションなどは無いので、現状では簡単なスキーマを定義するのみとなっています 👇
import * as z from "zod";
z.boolean(); // 単純なBoolean型のスキーマ
BigInt 型
BigInt 型スキーマを定義するにはz.bigint()
を使って定義します。
BigInt については、以下の MDN のドキュメントを参照してください。
また、この ZodType には Number 型のような特別なオプションはありません。
そのため、出来ることは単純な型スキーマを定義する事のみとなっています 👇
import * as z from "zod";
z.bigint(); // 単純なBigInt型のスキーマ
Date 型
Date 型スキーマを定義するにはz.date()
を使って定義します。
Date 型には固有のオプションなどは無いので、現状では簡単なスキーマを定義するのみとなっています 👇
import * as z from "zod";
z.date(); // 単純なDate型のスキーマ
Undefined 型
undefined
のスキーマを定義するにはz.undefined()
を使って定義します。
import * as z from "zod";
z.undefined(); // undefined
またoptional()
オプションを用いる事で、他の ZodType に付随させることが可能です。
z.string().optional(); // string | undefined
Null 型
null
のスキーマを定義するにはz.null()
を使って定義します。
import * as z from "zod";
z.null(); // null
またnullable()
オプションを用いる事で、他の ZodType に付随させることが可能です。
z.string().nullable(); // string | null
Void 型
void 型は、null | undefined
となるような型となっています。
void 型のスキーマを作成するにはz.void()
を使って定義します。
import * as z from "zod";
z.void(); // null | undefined
基本的にFunction 型を定義する際に使用するので、単体での使用頻度は少ないかと思います。
Any 型
any
のスキーマを作成するにはz.any()
を使って定義します。
import * as z from "zod";
z.any(); // any
z.any().parse("any"); // ✅ どの値でも検証に成功します
Never 型
never
のスキーマを作成するにはz.never()
を使って定義します。
import * as z from "zod";
z.never(); // never
z.never().parse("any"); // ❌ どの値も検証に失敗します
Literal 型
Literal 型のスキーマを作成するにはz.literal()
を使って定義しますが、実行する際は引数に検証したいを Literal 型の値を渡す必要があります 👇
import * as z from "zod";
z.literal(1); // 1 のスキーマ
z.literal("hoge"); // "hoge" のスキーマ
z.literal(10n); // 10n のスキーマ( 10nはBigInt型のこと )
z.literal(true); // true のスキーマ
z.literal(undefined); // undefined のスキーマ
z.literal(null); // null のスキーマ
Array 型
Array 型のスキーマを作成するにはz.array()
を使って定義しますが、実行する際は引数に ZodType を渡す必要があります。
Array 型にも便利なオプションがあります 👇
import * as z from "zod";
z.array(z.string()); // string[]
z.array(z.string()).min(5); // 5以上の要素を持つstring[]
z.array(z.string()).max(5); // 5以下の要素を持つstring[]
z.array(z.string()).length(5); // 5要素を持つstring[]
z.array(z.string()).nonempty(); // 空配列でないstring[]
また、array()
オプションを用いる事で、他の ZodType に付随させることが可能です。
z.string().array(); // string[]
z.string().optional().array(); // (string | undefined)[]
z.string().array().optional(); // string[] | undefined
Object 型
Object 型のスキーマを作成するにはz.object()
を使って定義しますが、実行する際は引数に ZodType を含んだオブジェクトを渡す必要があります 👇
import * as z from "zod";
// { name: string; age:number; } のスキーマ
z.object({
name: z.string(),
age: z.number(),
});
そして、この Object 型の ZodType には、他の ZodType とは少し違ったオプションが沢山ありますので、それらを一つ一つ解説したいと思います。
.shape
.shape
を用いる事で、プロパティに設定したスキーマを取得することが出来ます。
const schema = z.object({ str: z.string(), num: z.number() });
const str = schema.shape.str; // stringのスキーマ
const num = schema.shape.num; // numberのスキーマ
使いどころ
このオプションにより、Object 内部にスキーマをまとめ易くなります。
以下に例を示します。
const UserSchema = z.object({
name: z.string().max(50),
age: z.number().max(100),
});
const UserName = UserSchema.shape.name;
const UserAge = UserSchema.shape.age;
上記のようにすることで、UserSchema
を変更するだけでUserName
・UserAge
両方のスキーマを変更することが出来ます。これによって、わざわざUserName
・UserAge
を管理せずとも、UserSchema
との整合性を高めることが出来るので非常に便利なオプションとなっています。
.extend()
.extend()
オプションを使う事で、Object スキーマのプロパティを上書きしながら、新しい Object スキーマを作成することが出来ます。
const UserSchema = z.object({
id: z.number(),
name: z.string().max(50),
age: z.number().max(100),
});
// UserSchemaから管理者のスキーマを作成する
const AdminSchema = UserSchema.extend({
id: z.string(), // 既存のプロパティを上書き
isAdmin: z.boolean(), // 新規プロパティを追加
});
type UserType = z.infer<typeof UserSchema>;
/*
{
id: number;
name: string;
age: number;
}
*/
type AdminType = z.infer<typeof AdminSchema>;
/*
{
id: string; // number => stringに変化してる
name: string;
age: number;
isAdmin: boolean; // 新しく追加されている
}
*/
注意点としては、プロパティが被っている場合は上書きされてしまいますので、間違って上書きしてしまわないよう注意しましょう。また、引数には.shape
オプションの値も渡せますので、以下の方法でも書くことが可能です。
const HelloSchema = AnySchema.extend( WorldSchema.shape );
使いどころ
このオプションは、上記の例でも分かる通り、似たような構造を持ったスキーマを作る際に大変便利です。createdAt
・updatedAt
などのテーブルが共通して持っている要素などは、このオプションで定義する事をオススメします。
const CommonSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
deletedAt: z.date().optional(),
})
const UserSchema = CommonSchema.extend({/* -- 省略 -- */});
const TaskSchema = CommonSchema.extend({/* -- 省略 -- */});
.merge()
.merge()
オプションを使う事で、二つの Object スキーマから一つの Object スキーマを作成することが出来ます。
const HasIdSchema = z.object({ id: z.number() });
const BaseUserSchema = z.object({ id: z.string(), name: z.string() });
// HasIdSchemaとBaseUserSchemaからスキーマを作成
const UserSchema = BaseUserSchema.merge(HasIdSchema);
type UserType = z.infer<typeof UserSchema>;
/*
{
id: number; // プロパティは引数に渡された方が優先されます
name: string;
}
*/
注意点として、引数には Object スキーマしか渡せません。また被っているプロパティがあった場合は、引数に渡された Object スキーマの方が優先されます。
使いどころ
.extend()
オプションと似ていますが、引数に渡された Object スキーマの unknownKeys ポリシー( strip/strict/passthrough )を引き継ぐ点が違います。
.strict()
オプションを用いた場合を見てみます 👇
const A = z.object({ str: z.string() });
// 検証時に余計なプロパティがあった場合は、エラーを投げるようにする
const B = z.object({ num: z.number() }).strict();
// 要らない値を含んだオブジェクト
const value = { str: "", num: 0, hoge: "要らない値" };
A.extend( B.shape ).parse( value ); // ✅検証が通る
A.merge( B ).parse( value ); // ❌.strict()によってエラーになる
上記の挙動より、スキーマの条件を重要視している所では.merge()
を使い、そうで無い所では.extend()
を使うといった使い分けをしたらいいと思います。
筆者的には.extend()
でスキーマを作って、そのスキーマに unknownKeys ポリシーを設定する方が分かりやすいと思っています。実は.merge()
使ったことないなんて言え n...
.pick()
.pick()
オプションを使う事で、Object スキーマから特定のプロパティのみを取ってきて、一つの Object スキーマを作成することが出来ます。
const UserSchema = z.object({
id: z.number(),
name: z.string(),
task: z.object({
title: z.string()
})
});
// idとnameだけを取ってきてObjectスキーマを作る
const BasicUserSchema = UserSchema.pick({ id: true, name: true })
type BasicUserType = z.infer<typeof BasicUserSchema>;
/*
{
id: number;
name: string;
}
*/
使いどころ
reduxなどのデータが一枚岩となっているような場合は、このオプションを活用する事で柔軟にスキーマを定義できます。
具体的な所で言うと、フォームの状態管理では一枚岩のようなデータ構造になっていることが多いので、そのような場合に使いやすいと思います 👇
const FormValuesSchemas = z.object({
name: z.string(),
age: z.nubmer(),
tasks: z.string().array(),
});
// ユーザー情報を扱うスキーマ
const UserSchema = RootStoreSchemas.pick({ name: true, age: true });
// タスク情報を扱うスキーマ
const TaskSchema = RootStoreSchemas.pick({ tasks: true });
.omit()
.omit()
オプションを使う事で、Object スキーマから特定のプロパティを除外して、一つの Object スキーマを作成することが出来ます。
const UserSchema = z.object({
id: z.number(),
name: z.string(),
tasks: z.string().array(),
});
// tasksを除外してObjectスキーマを作る
const BasicUserSchema = UserSchema.omit({ tasks: true })
type BasicUserType = z.infer<typeof BasicUserSchema>;
/*
{
id: number;
name: string;
}
*/
使いどころ
ほとんど.pick()
と同じです。使いやすい方を使えばいいと思います 👇
const FormValuesSchemas = z.object({
name: z.string(),
age: z.nubmer(),
tasks: z.string().array(),
});
// ユーザー情報を扱うスキーマ
const UserSchema = RootStoreSchemas.omit({ tasks: true });
// タスク情報を扱うスキーマ
const TaskSchema = RootStoreSchemas.omit({ name: true, age: true });
.partial()
.partial()
オプションを使う事で、Object スキーマ内のプロパティ全てをオプショナルに出来ます。
const UserSchema = z.object({
name: z.string(),
age: z.number(),
location: z.object({
lat: z.number(),
long: z.number(),
})
})
// UserSchemaをオプショナルにしたスキーマ
const PartialUserSchema = UserSchema.partial();
type PartialUserType = z.infer<typeof PartialUserSchema>;
/*
{
name?: string | undefined;
age?: number | undefined;
location?: { lat: number; long: number } | undefined;
}
*/
使いどころ
具体的な例で言うと、更新 API に送信する値を検証する時などに大変便利です 👇
// ユーザー情報を更新する関数
// PartialUserSchemaなどは上記で定義したモノ
const updateUser = async (values: PartialUserType) => {
const updateValues = PartialUserSchema.parse(values); // 値を検証する
/* -- ここで更新処理をする -- */
}
.deepPartial()
.deepPartial()
オプションを使う事で、Object スキーマ内のプロパティの深い所までをオプショナルに出来ます。
const UserSchema = z.object({
name: z.string(),
age: z.number(),
location: z.object({
lat: z.number(),
long: z.number(),
})
})
// プロパティの深い所までオプショナルにしたスキーマ
const DeepPartialUserSchema = UserSchema.deepPartial();
type DeepPartialUserType = z.infer<typeof DeepPartialUserSchema>;
/*
{
name?: string | undefined;
age?: number | undefined;
location?: {
lat?: number | undefined;
long?: number | undefined;
} | undefined;
}
*/
使いどころ
基本的に.partial()
オプションと同じだと思います。
.passthrough()
.passthrough()
オプションを使う事で、検証結果に無関係なプロパティを含めることが出来ます。
言葉だけだと分かりづらいと思いますので、以下の例で挙動を確認してください 👇
const UserSchema = z.object({ name: z.string() });
// 関係ない値`age`を含んだ入力値
const values = { name: "", age: 0 };
// デフォルトの挙動
const defaultParse = UserSchema.parse(values);
console.log(defaultParse); // { name: "" }
// .passthrough()の挙動
const passParse = UserSchema.passthrough().parse(values);
console.log(passParse); // { name: "", age: 0 }
.strict()
.strict()
オプションを使う事で、検証時に無関係なプロパティがあった場合に検証失敗するように出来ます。
言葉だけだと分かりづらいと思いますので、以下の例で挙動を確認してください 👇
const UserSchema = z.object({ name: z.string() });
const values = { name: "", age: 0 };
// デフォルトの挙動
UserSchema.parse(values); // ✅ 検証に成功します
// .strict()の場合
UserSchema.strict().parse(values); // ❌ 検証に失敗します
// 成功させるには無関係な値を除外する必要があります
const { age, ...userValues } = values; // 無関係な値を除外する
UserSchema.strict().parse(userValues); // ✅ 検証に成功します
.strip()
.strip()
オプションを使う事で、検証時の挙動をデフォルトに戻すことが出来ます 👇
const StrictUserSchema = z.object({ name: z.string() }).strict();
const values = { name: "", age: 0 };
StrictUserSchema.parse(values); // ❌ 検証に失敗します
StrictUserSchema.strip().parse(values); // ✅ 検証に成功します
.catchall()
.catchall()
オプションを使う事で、検証時の無関係なプロパティを検証することが出来ます 👇
const UserSchema = z.object({ name: z.string() }).catchall(z.number());
UserSchema.parse({ name: "", age: 0 }); // ✅ 検証に成功します
UserSchema.parse({ name: "", age: false }); // ❌ 検証に失敗します
注意点としては、.catchall()
オプションで検証されたプロパティは、無関係なプロパティとは扱われなくなりますので、.passthrough()
・.strip()
・.strict()
で検知する事が出来無くなります。
const UserSchema = z.object({ name: z.string() }).catchall(z.number());
const values = { name: "", age: 0 };
// .strict()の場合
const result = UserSchema.strict().parse(values); // ✅ 検証に成功します
console.log(result); // { name: "", age: 0 }
// .passthrough()の場合
const result2 = UserSchema.passthrough().parse(values); // ✅ 検証に成功します
console.log(result2); // { name: "", age: 0 }
// .strip()の場合
const result3 = UserSchema.strip().parse(values); // ✅ 検証に成功します
console.log(result3); // { name: "", age: 0 }
Union 型(共用型)
Union 型のスキーマを作成するにはz.union()
を使って定義します。実行する際は引数に ZodType のみを含んだ配列を渡す必要があります。
import * as z from "zod";
z.union([z.string(), z.number()]); // string | number
z.union([z.string().nullable(), z.number()]); // string | number | null
// ZodTypeに被りがある場合は統合されます
z.union([z.string(), z.string()]); // string
また、.or()
オプションを用いる事で、他の ZodType に付随させることが可能です。
const StrOrNum = z.string().or(z.number()); // string | number
Intersection 型(交差型)
Intersection 型のスキーマを作成するにはz.intersection()
を使って定義します。実行する際は引数に ZodType のみを含んだ配列を渡す必要があります。
import * as z from "zod";
const a = z.union([z.number(), z.string()]); // number | string
const b = z.union([z.number(), z.boolean()]); // number | boolean
z.intersection(a, b); // number
また、.and()
オプションを用いる事で、他の ZodType に付随させることが可能です。
const MergeSchema = obj1.and(obj2); // { a: string } & { b: number }
注意として、多くの場合 2 つのオブジェクトをマージするなら.merge()
オプションを使用する事をオススメします。Intersection 型のスキーマには、Object 型の便利なオプションなどが無いため、他のスキーマとの相性があまり良くありません。
Enum 型
Enum 型のスキーマには、Zod enums
とNative enums
の二つ種類があります。
Zod enums
このZod enums
は Zod 独自のモノですが、Enum 型を定義または検証する時に推奨されています。
この Zod 独自の enum を定義するには、z.enum()
を使って定義します。
注意として、引数の値には文字列のみを含んだ配列かつ、直接値を渡す必要があります 👇
import * as z from "zod"
z.enum(["Salmon", "Tuna", "Trout"]); // 'Salmon' | 'Tuna' | 'Trout'
// 以下のようには書けません🙅♂️🙅♀️
const fish = ["Salmon", "Tuna", "Trout"];
z.enum(fish); // ❌ 型エラーが発生します
また、.enum
オプションを用いる事で enum 値にアクセスすることができ、.options
オプションを使う事で、Tuple 型に変換できます。
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
type FishType = typeof FishEnum.enum;
/*
{
Salmon: "Salmon",
Tuna: "Tuna",
Trout: "Trout",
}
*/
// EnumをTupleに変換する
type FishTuple = typeof FishEnum.options;
/*
["Salmon", "Tuna", "Trout"]
*/
Native enums
Native enums
のスキーマを定義するにはz.nativeEnum()
を使って定義します。
サードパーティライブラリの列挙型に対して検証する必要がある場合は、こちらを使う事で対応可能です。
Numeric enums
enum Fruits {
Apple,
Banana,
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // ✅ 検証に成功します
FruitEnum.parse(Fruits.Banana); // ✅ 検証に成功します
FruitEnum.parse(0); // ✅ 検証に成功します
FruitEnum.parse(1); // ✅ 検証に成功します
FruitEnum.parse(3); // ❌ 検証に失敗します
String enums
enum Fruits {
Apple = "apple",
Banana = "banana",
Cantaloupe = 0, // Numeric enumsとString enumsは、混在させることができます
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // ✅ 検証に成功します
FruitEnum.parse(Fruits.Cantaloupe); // ✅ 検証に成功します
FruitEnum.parse("apple"); // ✅ 検証に成功します
FruitEnum.parse("banana"); // ✅ 検証に成功します
FruitEnum.parse(0); // ✅ 検証に成功します
FruitEnum.parse("Cantaloupe"); // ❌ 検証に失敗します
Const enums
as const
を用いたオブジェクトも扱うことが出来ます。
const Fruits = {
Apple: "apple",
Banana: "banana",
Cantaloupe: 3,
} as const;
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // "apple" | "banana" | 3
FruitEnum.parse("apple"); // ✅ 検証に成功します
FruitEnum.parse("banana"); // ✅ 検証に成功します
FruitEnum.parse(3); // ✅ 検証に成功します
FruitEnum.parse("Cantaloupe"); // ❌ 検証に失敗します
Tuples 型
Tuples 型のスキーマを定義するにはz.tuple()
を使って定義します。
引数には ZodType のみを含んだ配列しか渡せない事に注意してください!
また.items
オプションを用いる事で、要素のスキーマにアクセスする事が出来ます 👇
import * as z from "zod";
z.tuple([ z.string(), z.number() ]); // [ string, number ]
z.tuple([ z.string(), z.number().nullable() ]); // [ string, number | null ]
// 要素のスキーマを取得
const ItemsSchema = z.tuple([ z.string(), z.number() ]).items;
type ItemsType = typeof ItemsSchema;
/*
[ z.ZodString, z.ZodNumber ]
*/
Optional 型
Optional 型のスキーマを定義するにはz.optional()
を使って定義します。
import * as z from "zod";
z.optional(z.string()); // string | undefined
.optional()
オプションを用いる事で、他の ZodType に付随させることが可能です。
また.unwrap()
オプションを用いる事で、元に戻すことが出来ます。
z.string().optional(); // string | undefined
z.string().optional().unwrap(); // string
// .unwrap()が返すスキーマは元のスキーマと同じ値になります
const StringSchema = z.string();
const OptionalString = StringSchema.optional();
OptionalString.unwrap() === StringSchema; // true
Nullables 型
Nullables 型のスキーマを定義するにはz.nullable()
を使って定義します。
import * as z from "zod";
z.nullable(z.string()); // string | null
.nullable()
オプションを用いる事で、他の ZodType に付随させることが可能です。
また.unwrap()
オプションを用いる事で、元に戻すことが出来ます 👇
z.string().nullable(); // string | null
z.string().nullable().unwrap(); // string
// .unwrap()が返すスキーマは元のスキーマと同じ値になります
const StringSchema = z.string();
const NullableString = StringSchema.nullable();
NullableString.unwrap() === StringSchema; // true
Records 型
Records 型のスキーマを定義するにはz.record()
を使って定義します。
import * as z from "zod";
z.record(z.number()); // { [k: string]: number }
数値キーに関しての注意
z.record()
のキーの型は String 型となっていますが、これを Number 型にしたい場合があるかもしれません。しかし、Zod はこれをサポートしていません。
理由は、JavaScript はすべてのオブジェクトキーを内部の文字列に自動的にキャストするためサポートする意味がないためです。以下の挙動を見れば分かりやすいと思います 👇
const testMap: { [k: number]: string } = {
1: "one",
};
for (const key in testMap) {
console.log(`${key}: ${typeof key}`); // prints: `1: string`
}
上記の挙動より、本来 Number 型のkey
がfor..in
文内では String 型に変換されています。README.md にも言及がありますので、以下引用させてもらいます。
Since Zod is trying to bridge the gap between static and runtime types, it doesn't make sense to provide a way of creating a record schema with numerical keys, since there's no such thing as a numerical key in runtime JavaScript.
要約
Zod は静的な型とランタイムの型のギャップを無くしたいので、Number 型のキーはサポートしません。
Maps 型
Maps 型のスキーマを定義するにはz.map()
を使って定義します。
import * as z from "zod";
z.map(z.string(), z.number()); // Map<string, number>
Map の詳細については、以下のサイトを参照してください 👇
Sets 型
Sets 型のスキーマを定義するにはz.set()
を使って定義します。
import * as z from "zod";
z.set(z.string()); // Set<string>
Set の詳細については、以下のサイトを参照してください 👇
Function 型
Function 型のスキーマを作成するにはz.function()
を使って定義しますが、こちらの ZodType は関数を検証するモノではなく、関数の入力と出力を検証するモノとなっています。
そのため、他の ZodType のように.parse()
を使用するのではなく、.implement()
を用いて関数を定義する際に使用します。
import * as z from "zod";
// スキーマの定義方法
z.function(); // () => unknown
z.function().args(z.number()); // (args_0: number) => unknown
z.function().returns(z.boolean()); // () => boolean
// 以下のようにも定義できます。
const returnSchema = z.string();
const argsSchema = z.tuple([z.number(), z.number()]);
z.function(argsSchema, returnSchema);
// (args_0: string, args_1: number) => string
// スキーマが何も返さない事を示すにはz.void()を使用します
z.function().returns(z.void()); // () => void
// Functionスキーマを使って関数を定義する
const add = z
.function()
.args(z.number(), z.number())
.returns(z.number())
.implement((a, b) => {
return a + b; // a,bは検証済みの値
});
add(1, 2); // ✅ 実行できる
add("1", 2); // ❌ 検証に失敗してエラーが発生します
上記のadd()
ように Function スキーマを使って関数を実装する事で、コンパイル後の JavaScript でも型の整合性を保つこと出来ます。これにより、検証コードとビジネスロジックを混同することなく、関数の入力と出力を簡単に検証できています。
また、引数や返り値のスキーマを取得する事もできます 👇
const FuncSchema = z
.function()
.args(z.string(), z.number())
.returns(z.boolean());
// スキーマの型の確認
type FuncType = z.infer<typeof FuncSchema>;
// (arg0: string, arg1: number) => boolean
// 引数のスキーマを取得
FuncSchema.parameters(); // ZodTuple<[ZodString, ZodNumber]>
// 返り値のスキーマを取得
FuncSchema.returnType(); // ZodBoolean
Promise 型
Promise 型のスキーマを定義するにはz.promise()
を用いて定義します。
注意点として、Promise 型のスキーマの.parse()
は特別な挙動をします。
検証には、以下の 2 つが行われます。
- 入力値が Promise インスタンスであるかチェックします。(
.then()
か.catch()
を持つオブジェクト ) - Zod は
.then()
を用いて、検証ステップを既存の Promise に追加します。
上記を踏まえて、スキーマの定義は以下のようにします 👇
import * as z from "zod";
// Promiseスキーマを定義
const numberPromise = z.promise(z.number());
// ❌ 入力値がPromiseでないため、検証に失敗します
numberPromise.parse(0); // ZodError: Non-Promise type: number
// Promiseを返します( 返されたPromise内でエラーが発生します )
numberPromise.parse(Promise.resolve("tuna")); // Promise<number>
// async/awiatを使って検証できます
const test = async () => {
await numberPromise.parse(Promise.resolve("tuna"));
// ZodError: Non-number type: string
await numberPromise.parse(Promise.resolve(3.14));
// => 3.14
// 検証失敗時の結果を取得するには`.catch()`を使用します
await numberPromise.parse("invalid").catch((errorResult) => {
/* ... */
});
};
Recursive Types(再帰型)
再帰的なスキーマを定義するにはz.lazy()
を用いて定義します。
注意点として、TypeScript の制限によりスキーマの型を静的に推測することはできません。そのため、型を手動で定義する必要があります 👇
import * as z from "zod";
interface Category {
name: string;
subcategories: Category[];
}
// cast to z.ZodSchema<Category>
const CategorySchema: z.ZodSchema<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema),
})
);
CategorySchema.parse({
name: "People",
subcategories: [
{
name: "Politicians",
subcategories: [{ name: "Presidents", subcategories: [] }],
},
],
}); // ✅ 検証に成功します
JSON Type
JSON の値を検証するには、以下のスニペットを使用することが出来ます。
type Literal = boolean | null | number | string;
type Json = Literal | { [key: string]: Json } | Json[];
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);
jsonSchema.parse(data);
Instanceof
instanceof
演算子を使ったスキーマも作る事が可能です。z.instanceof()
でスキーマを定義します 👇
import * as z from "zod";
class Test {
name: string;
}
// instanceofのスキーマを作成
const TestSchema = z.instanceof(Test);
const blob: any = "whatever";
TestSchema.parse(new Test()); // ✅ 検証に成功します
TestSchema.parse("blob"); // ❌ 検証に失敗します
Template Literal
.custom()
を用いる事で、TypeScript のTemplate Literalを扱うことができます。
以下のソースコードは、Issueの回答の引用です 👇
const literal = z.custom<`${number}.${number}.${number}`>((val) =>
/^\d+\.\d+\.\d+$/g.test(val as string)
);
Custom Validation
独自のバリデーションを実装したい場合は、.refine()
オプションを使う事で定義できます。
import * as z from "zod";
// 入力値がanyの場合
z.any().refine(
(value: any) => value === "hoge",
(value: any) => ({ message: `${value} is not "hoge"` })
);
// 入力値がstringの場合
z.string().refine(
(value: string) => value === "hoge",
(value: string) => ({ message: `${value} is not "hoge"` })
);
.superRefine()
.refine()
では一つの検証情報しか返せませんでしたが、.superRefine()
を使う事で複数の検証情報を返すことが出来ます 👇
const Strings = z.array(z.string()).superRefine((val, ctx) => {
// 配列の要素数を検証して、検証結果を登録する
if (val.length > 3) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 3,
type: "array",
inclusive: true,
message: "要素が多すぎます😡",
});
}
// 配列要素の重複を検証して、検証結果を登録する
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "重複は認められません",
});
}
});
共通のオプションについて
上記で解説した ZodType には共通のオプションが用意されています。
この節では、それらのオプションについて解説したいと思います。
.default()
スキーマにデフォルト値を設定します。
デフォルト値を設定している場合は、undfined
を入力値として許容する事に注意してください!
const schema = z.string().default("default value");
type SchemaType = z.infer<typeof schema>; // string
const result = schema.parse(void 0);
console.log(result); // "default value"
const result2 = schema.parse("hoge");
console.log(result2); // "hoge"
.optional()
スキーマの入力値にundefined
を追加します。
const schema = z.string().optional();
type SchemaType = z.infer<typeof schema>; // string | undefined
const result = schema.parse(void 0);
console.log(result); // undefined
const result2 = schema.parse("hoge");
console.log(result2); // "hoge"
.nullable()
スキーマの入力値にnull
を追加します。
const schema = z.string().nullable();
type SchemaType = z.infer<typeof schema>; // string | null
const result = schema.parse(null);
console.log(result); // null
const result2 = schema.parse("hoge");
console.log(result2); // "hoge"
.array()
スキーマを Array 型に変換します。
const schema = z.string().array();
type SchemaType = z.infer<typeof schema>; // string[]
const result = schema.parse([]);
console.log(result); // []
const result2 = schema.parse(["hoge"]);
console.log(result2); // ["hoge"]
Array 型の詳細については前述してありますので、そちらを参照してください。
.or()
スキーマを Union 型に変換します。
const schema = z.string().or( z.number() );
type SchemaType = z.infer<typeof schema>; // string | number
const result = schema.parse("hoge");
console.log(result); // "hoge"
const result2 = schema.parse(100);
console.log(result2); // 100
Union 型の詳細については前述してありますので、そちらを参照してください。
.and()
スキーマを Intersection 型に変換します。
const a = z.union([z.number(), z.string()]); // number | string
const b = z.union([z.number(), z.boolean()]); // number | boolean
a.and(b); // number
Intersection 型の詳細については前述してありますので、そちらを参照してください。
検証方法について
この節では、検証を実行するためのオプション(関数)について解説したいと思います 🗃
.parse()
.parse()
を実行する事で、入力値が期待する値かを検証することが出来ます。
検証成功時には、検証した値( 期待した型の値 )を返し、検証に失敗した場合はエラーが投げられます 👇
import * as z from "zod";
// String型のスキーマ
const StringSchema = z.string();
StringSchema.parse("fish"); // ✅ 検証に成功し、"fish"を返します
StringSchema.parse(12); // ❌ 検証に失敗し、エラーが投げられます
// エラー情報を受け取るにはtry..catch文を使う必要があります
try {
StringSchema.parse(true);
} catch(error) {
console.error(error); // ZodError('Non-string type: boolean');
}
.parseAsync()
.refine()
や.transform()
を使った非同期スキーマを検証する場合、.parseAsync()
を使って検証する必要があります。
import * as z from "zod";
// 非同期スキーマを作成
const HelloSchema = z.string().refine(async (val) => val === "hello");
(async () => {
await HelloSchema.parseAsync("hello"); // ✅ 検証に成功し、"hello"を返します
await HelloSchema.parseAsync("world"); // ❌ 検証に失敗し、エラーが投げられます
})();
.safeParse()
エラーを投げずに検証するには、.safeParse()
を使う事で検証できます。
import * as z from "zod";
// String型のスキーマ
const StringSchema = z.string();
const result = StringSchema.safeParse("fish"); // ✅ 検証に成功
console.log(result); // { success: true, data: "fish" }
const result = StringSchema.safeParse(12); // ❌ 検証に失敗
console.log(result); // { success: false, data: ZodError }
検証結果は、Union 型であるため以下のような記述で型情報を適切に適用できます。
const result = stringSchema.safeParse("billie");
type ResultType = typeof result;
// { result: true; data: string } | { result: false; error: ZodError }
if (result.success === true) result.error; // 🛑 型エラーが発生します
if (result.success === false) result.data; // 🛑 型エラーが発生します
.safeParseAsync()
.refine()
や.transform()
を使った非同期スキーマでエラーを投げずに検証する場合、.safeParseAsync()
を使って検証する必要があります。また、エイリアスとして.spa()
があります。
import * as z from "zod";
// 非同期スキーマを作成
const HelloSchema = z.string().refine(async (val) => val === "hello");
(async () => {
const result = await HelloSchema.safeParseAsync("hello"); // ✅ 検証に成功します
console.log(result); // { success: true, data: "hello" }
const result = await HelloSchema.safeParseAsync("world"); // ❌ 検証に失敗します
console.log(result); // { success: false, error: ZodError }
})();
// 以下のように`.spa()`を用いても同じです
(async () => {
const result = await HelloSchema.spa("hello"); // ✅ 検証に成功します
console.log(result); // { success: true, data: "hello" }
const result = await HelloSchema.spa("world"); // ❌ 検証に失敗します
console.log(result); // { success: false, error: ZodError }
})();
Transformer について
Zod には、検証した値を別の値に変換するTransformer
という機能があります。
以下の例では、文字列を数値に変換しています 👇
import * as z from "zod";
// 文字列から数値へ変換するスキーマ
const stringToNumber = z.string().transform((val) => myString.length);
const result = stringToNumber.parse("string"); // ✅ 検証が成功します
console.log(result); // 6
メソッドチェーンの順番について
上記のstringToNumber
は、ZodEffects
のサブクラスのインスタンスであることに注意してください!z.string()
と同じインスタンスではありません。
そのため、z.string()
が持つ.email()
などのオプションを適用するには.transform()
を実行する前に実行する必要があります。
const emailToDomain = z
.string()
.email()
.transform((val) => val.split("@")[1]);
const result = emailToDomain.parse("colinhacks@example.com");
console.log(result); // "example.com"
// ❌ 以下のような書き方は出来ません
const emailToDomain = z
.string()
.transform((val) => val.split("@")[1]) // 返す値に.email()が無い!
.email();
非同期の Transformer
Transformer
を使って、非同期の変換処理を定義することが出来ます 👇
const IdToUser = z.string().uuid().transform(
async (userId) => {
return await getUserById(userId); // userIdからユーザー情報へ変換する
}
);
.refine()との連携
.refine()
を使う事で変換後の値を検証することが出来ますが、これは上記の非同期的に変換したデータを検証する時に便利なモノとなっています 👇
// ユーザーのスキーマを定義
const UserSchema = z.object({
id: z.string(),
name: z.string(),
});
// ユーザーIDをユーザー情報に変換するスキーマ
const IdToUser = UserSchema.shape.id.transform(
async (userId) => {
return await getUserById(userId); // userIdからユーザー情報へ変換する
}
);
IdToUser
.refine(user => UserSchema.safeParse(user).success, "不正なユーザー情報です")
.parse("example-user-id")
.then(user => {
console.log(user); // { id: "example-user-id", name: "example user" }
})
例えば、以下の処理は正しく検証できません 👇
IdToUser
.refine(user => {
// .parse()は検証失敗時にエラーを投げるが、
// .refine()はそれを検知しないため必ず検証に成功してしまう
return UserSchema.parse(user)
}, "不正なユーザー情報です")
Error Handling について
この節では、検証失敗時に返される Error クラスや、その扱い方について解説していきたいと思います 🌌
※ 基本的に以下の公式ドキュメントを参照しています 👇
ZodError
検証失敗時、Zod はError クラスを継承したZodError クラスのインスタンスを返します。
この ZodError には、検証の詳細情報を含んだissues
オプションがあり、このオプションを使う事で詳細なエラーハンドリングを行う事が可能です 👇
try {
z.never().parse(1); // ❌ 必ず検証に失敗します
} catch (zodError: z.ZodError) {
// 検証に失敗した理由をログに表示
console.error(zodError.issues);
/*
[
{
"code": "invalid_type", // 検証タイプ( タイプ一覧は下記参照 )
"expected": "never", // 期待した値の型
"received": "number", // 入力値の型
"path": [], // エラー箇所へのパス
"message": "Expected never, received string" // エラーの詳細文
},
// ...
]
*/
}
// Zodのエラーコード一覧
type ZodIssueCode =
| "invalid_type"
| "custom"
| "invalid_union"
| "invalid_enum_value"
| "unrecognized_keys"
| "invalid_arguments"
| "invalid_return_type"
| "invalid_date"
| "invalid_string"
| "too_small"
| "too_big"
| "invalid_intersection_types";
IssueCode
の詳細な説明は、以下の公式ドキュメントを参照してください。
エラー文のカスタマイズについて
一部のオプションでは、引数にカスタマイズしたエラー文を渡すことが出来ます。
以下に設定できるオプションの一覧と設定方法を載せておきますので、参考にして下さい 👇
import * as z from "zod";
// .refine()の設定方法
z.any().refine((input) => input, "hoge");
z.any().refine((input) => input, { message: "hoge" });
z.any().refine(
(input) => input,
(input) => ({ message: `${input} is not hoge` })
);
// z.string()のオプション
z.string().min(5, "カスタマイズしたエラー文");
z.string().max(5, "カスタマイズしたエラー文");
z.string().length(5, "カスタマイズしたエラー文");
z.string().url({ message: "カスタマイズしたエラー文" });
z.string().uuid({ message: "カスタマイズしたエラー文" });
z.string().email({ message: "カスタマイズしたエラー文" });
z.string().nonempty({ message: "カスタマイズしたエラー文" });
z.string().regex(/.*/, { message: "カスタマイズしたエラー文" });
// z.number()のオプション
z.number().min(5, "カスタマイズしたエラー文");
z.number().max(5, "カスタマイズしたエラー文");
z.number().int({ message: "カスタマイズしたエラー文" });
z.number().positive({ message: "カスタマイズしたエラー文" });
z.number().negative({ message: "カスタマイズしたエラー文" });
z.number().nonpositive({ message: "カスタマイズしたエラー文" });
z.number().nonnegative({ message: "カスタマイズしたエラー文" });
// z.array()のオプション
z.array(z.any()).min(5, "カスタマイズしたエラー文");
z.array(z.any()).max(5, "カスタマイズしたエラー文");
z.array(z.any()).length(5, { message: "カスタマイズしたエラー文" });
型エラーなどのエラー文のカスタマイズ
型エラーやオプションがそもそもないスキーマのエラー文を設定するには、z.setErrorMap()
を使ってエラー文を設定することが出来ます 👇
import * as z from "zod";
type ReturnValue = { message: string };
type Issue = Omit<z.ZodIssue, "message">;
type Context = { defaultError: string; data: any };
// エラー情報をカスタマイズする関数
const customErrorMap = (issue: Issue, ctx: Context): ReturnValue => {
// 型情報で判定
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "文字列型ではありません" };
}
}
if (issue.code === z.ZodIssueCode.custom) {
// 検証条件でエラー文を作成
return { message: `${(issue.params || {}).minimum}以下です` };
}
// デフォルトの値を返す
return { message: ctx.defaultError };
};
// customErrorMapを設定する
z.setErrorMap(customErrorMap);
型生成について
ZodType で生成したスキーマから TypeScript の型を生成することが出来ます!
使い方は簡単で、z.infer<>
の型引数にスキーマの型を入れるだけで生成できます!
import * as z from "zod";
const A = z.string();
type A = z.infer<typeof A>; // string
const u: A = 12; // TypeError
const u: A = "asdf"; // compiles
Transformer との兼ね合い
Transformerを使っている場合、入力値と出力値の型情報が違う事があります。
そのため、z.infer<>
は以下のような挙動になる事に注意してください 👇
// string => number への変換スキーマ
const stringToNumber = z.string().transform(val => val.length)
// ⚠️ z.infer は出力値の型を生成します!
type type = z.infer<stringToNumber>; // number
// ℹ️ 出力値の型を生成するにはz.outputを使用します
type out = z.output<stringToNumber>; // number, z.inferと同等
// ℹ️ 入力値の型を生成するにはz.inputを使用します
type in = z.input<stringToNumber>; // string, 入力値の型を生成します
型引数の渡し方
Zod を段階的に導入したい人で、z.object()
などに既に作成済みの型情報をz.object<AnyType>()
のような型引数に渡す感じでスキーマを定義したい人が居るかもしれません。
これを行うには、z.object()
に渡せるようにするtoZod
を作成して以下のようにします 👇
import * as z from "zod";
// zodに渡せる型に変換する型
type toZod<T extends Record<string, any>> = {
[K in keyof T]-?: z.ZodType<T[K]>;
}
interface IHoge {
hello: string;
world: string;
}
// Hogeを型引数として渡す
const HogeSchema = z.object<toZod<IHoge>>({
hello: z.string(),
world: z.string()
})
これにより、Hoge
の変更内容をz.object()
に伝える事が出来ます。
例えば、以下のようにHoge
の要素を変更した場合、z.object()
内で型エラーが発生します。
import * as z from "zod";
// zodに渡せる型に変換する型
type toZod<T extends Record<string, any>> = {
[K in keyof T]-?: z.ZodType<T[K]>;
}
interface IHoge {
hello: string;
// worldをコメントアウト
// world: string;
}
// Hogeを型引数として渡す
const HogeSchema = z.object<toZod<IHoge>>({
hello: z.string(),
world: z.string() // ❌ 型エラーが発生します!
})
react-hook-form との連携
フォームのバリデーションライブラリとして有名な react-hook-form との連携が可能です。
公式 READMEにサンプルコードがありましたので、以下に引用します。
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const schema = z.object({
name: z.string().nonempty({ message: 'Required' }),
age: z.number().min(10),
});
const App = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<input {...register('name')} />
{errors.name?.message && <p>{errors.name?.message}</p>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age?.message && <p>{errors.age?.message}</p>}
<input type="submit" />
</form>
);
};
export default App;
あとがき
以上、ここまで Zod の基本的な使い方から詳細な挙動について紹介してきました。
この記事で Zod を知る人が増える機会を作れたのなら本望です。
これが誰かの参考になれば幸いです。
もし記事に間違いなどがあれば、コメントなどで教えて頂けると嬉しいです。
ここまで読んでくれてありがとうございました 🙏
さて、ここからは余談ですが、TypeScript を怖いと言っていた知人に、この記事の解説部分のみを見せてみました。するとどうでしょう、「 あー、怖い、怖い 😫 」と言いながら見るのを断るではありませんか!
まさかの反応に、「 えっ、本当に怖いの? 」と知人に問うと、
「 こんなの誰だって、読むの怖いでしょ。 」
と言ってきました。
「 えっ、怖くはないでしょ。技術記事なんだし。怖い要素一つもなくない? 」
と私が言うと、知人が気付いて私にこう言いました。
「 あれ?もしかして "こわい" を "怖い" と勘違いしてない? 」
「 "こわい" っていうのは、"だるい" っていう意味だったんだけど。。。 」
それを聞いて、「 あー、なるほど。」と今までの食い違いを理解しました。
その後話を聞くと、「 こわい 」は北海道の方言で「 だるい・疲れる 」と言う意味になるそうで、知人は「 エンジニアとして一番怖いモノは何? 」と言う私の質問を、「 エンジニアとして一番疲れるモノは何? 」と解釈して答えたようです。
そりゃ確かに、JavaScript に比べたら TypeScript は結構疲れる所はありますよね 😅
解釈の食い違いも解決したところで一件落着!・・・ではありません 😡
私はこの記事を書くために、休日のほとんどを潰しました。それなのに、こんなオチで許すほど私はお人好しではありません!
腹の虫がおさまらない私は、一矢報いるべく知人に再度問い質しました。
「 それじゃあ、本当に怖いモノって何? 」
すると、知人が一言。
「 俺を怖がらせようとするお前の行動力にゾッとしとるわ。 」
(´・ω・`) ...
それではまた 👋
Discussion