この記事を共有する
Sébastien TIMONER
Web開発と技術チーム管理のエキスパートとして、高性能なデジタルソリューションの作成と最適化を専門としています。React.js、Node.js、TypeScript、Symfony、Docker、そしてFrankenPHPなどの最新技術における豊富な専門知識を活かし、様々な業界の企業向けに、設計から本番環境までの複雑なSaaSプロジェクトの成功を確実にします。
TypeScriptのユーティリティ型は、型変換を容易にする事前定義されたジェネリック型です。これらを使用することで、既存の型から新しい型を作成できます。例えば、特定のプロパティをオプショナルにしたり、関数の戻り値の型を抽出したりすることができます。この教育ガイドでは、3つのレベル(初心者、中級者、上級者)で、すべての標準的なTypeScriptユーティリティ型(Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、NonNullable、Parameters、ReturnType、InstanceType、ThisTypeなど)を紹介します。各型について、その有用性の説明、TypeScriptでの使用例、そしてZod(実行時バリデーションスキーマライブラリ)での同等のアプローチを比較します。また、TypeScriptユーティリティ型(静的型システム)とZodスキーマ(実行時バリデーション)の哲学的な違いについても議論します。
コードスニペットを試すには、Bun(超高速なJavaScriptランタイムで、パッケージマネージャーとしても機能)を使用して最小限のTypeScriptプロジェクトを初期化できます。Bunがインストールされていることを確認し、空のフォルダで以下を実行してください:
shell
Bunはデフォルトのpackage.jsonとtsconfig.jsonを作成します。その後、bun run file.tsでTypeScriptファイルを実行できます(BunはTypeScriptをオンザフライでトランスパイルします)。
ユーティリティ型の探索の準備が整いました!
TypeScript: Partial<T>ユーティリティ型は、既存の型Tから、そのすべてのプロパティをオプショナルにした型を構築します。つまり、Tの各プロパティがオプショナル(?を追加)になります。これにより、その型の部分的なオブジェクトを表現できます。例えば、必須フィールドを持つUser型がある場合、Partial<User>を使用すると、これらのフィールドを省略できるバージョンを作成できます:
typescript
ここで、Partial<User>は{ name?: string; age?: number }という型を持ちます。これにより、例えば、すべてのフィールドを必要としない部分更新関数を実装できます:
typescript
updateUser(existingUser, { age: 26 })を部分オブジェクトで呼び出すことができます – TypeScriptは静的にfieldsToUpdateがUserの有効なキーのみを含むことを保証します。
Zodでの同等の実装: Zodはオブジェクトスキーマを部分的なものにする同等のメソッドを提供します。z.objectスキーマに適用される.partial()メソッドは、TypeScriptのPartialと同様に、すべてのキーをオプショナルにします。例えば:
typescript
ここでPartialUserSchemaは、キーのサブセットのみを含む可能性のあるオブジェクトを検証するスキーマです(nameとageがオプショナルになります)。以下のように動作を確認できます:
typescript
Zodはまた、オブジェクトをパラメータとして渡すことで特定のプロパティのみをオプショナルにすることもできます(UserSchema.partial({ age: true })はageのみをオプショナルにします)。.partial()は最初のレベルでのみ作用することに注意してください。Zodはまた、ネストされたフィールドを再帰的にオプショナルにする.deepPartial()も提供します。
TypeScript: Required<T>ユーティリティ型はPartialの逆で、Tからすべてのプロパティを必須(非オプショナル)にした型を作成します。プロパティがオプショナルだった型を「固める」のに役立ちます。例えば:
typescript
この例では、Required<Settings>は{ theme: string; fontSize: number }と同等です – プロパティの省略はコンパイルエラーになります。
Zodでの同等の実装: Zodスキーマでは、同等のものは.required()メソッドで、オブジェクトスキーマを変換してすべてのプロパティを必須にします(.partial()がオプショナルにするのとは逆)。このメソッドは、最初に部分的なスキーマを定義し、後で厳格なバージョンを取得したい場合に特に便利です。例えば:
typescript
ここでStrictUserSchemaはnameとageが存在することを要求します。特定のプロパティのみを指定することもできます:BaseUserSchema.required({ age: true })はageのみを必須にし(nameはオプショナルのまま)、nameはオプショナルのままにします。
Required<T>はコンパイル時に作用することに注意してください(出力されるJavaScriptコードには存在しません)、一方Zodのschema.required()は実行時に作用し、パース時にキーの存在を検証します。
TypeScript: Readonly<T>ユーティリティ型は、型Tを変換してそのプロパティを読み取り専用(不変)にします。これらのプロパティを再代入しようとすると、コンパイル時にエラーとして報告されます。例えば:
typescript
ここで、todoはReadonly<Todo>型なので、初期化後にそのフィールドを変更することはできません。これはJavaScriptのObject.freezeの使用に対応することが多いですが、TypeScriptは型レベルでのみこれを処理します(実行時に実際に変更を防ぐわけではなく、コンパイル時に警告するだけです)。
Zodでの同等の実装: Zodは不変性のための興味深い機能を提供します。スキーマに適用される.readonly()メソッドは、パース時に結果に対してObject.freeze()を呼び出し、その静的型がReadonly<...>としてマークされた新しいスキーマを返します。つまり、Zodは検証されたオブジェクトを凍結し、それがreadonly型に対応することを保証できます。例えば:
typescript
ここで、FrozenUserSchemaはparseから出力されたオブジェクトを凍結します。frozenUserを変更しようとすると、オブジェクトが不変であるため実行時に例外がスローされます。したがって、ZodはTypeScriptよりも一歩進んでいます:Readonly<T>が純粋に静的であるのに対し(型付けを回避すればJSでプロパティを変更できる可能性がある)、Zodの.readonly()スキーマは対応するReadonly<...>型を提供するだけでなく、実行時に実際の不変性を保証します。
TypeScript: Pick<T, Keys>は、Tから特定のプロパティ(通常はプロパティ名を表す文字列リテラルのユニオン)のみを保持して新しい型を構築します。既存の型からより制限された型を作成するのに役立ちます。例えば:
typescript
ここで、TodoPreviewはFullTodoからtitleとcompletedのみを保持します。リストされていないプロパティ(例:description)は結果の型から除外されます。
Zodでの同等の実装: Zodオブジェクトスキーマにも、同様の方法で指定されたキーのみを含む新しいスキーマを作成する.pick()メソッドがあります:
typescript
保持するキーをtrueに設定したオブジェクトを渡します。TodoPreviewSchemaはtitleとcompletedを持つオブジェクトのみを検証します(FullTodoの他のプロパティはデフォルトでパーサーによって無視されます)。例えば、TodoPreviewSchema.parse({ title: "Clean", completed: false, description: "..." })は{ title: "...", completed: false }を返し、descriptionを削除します(デフォルトではZodは未指定のキーを無視します。この動作は.strict()や.passthrough()で必要に応じて調整できます)。
同様に、Zodは反対の効果のために.omit()を提供します。
TypeScript: Omit<T, Keys>は、Tから特定のプロパティ(Keys)を削除して新しい型を作成します。Pickの補完です(実際、Omit<T, K>はしばしばPick<T, Exclude<keyof T, K>>として実装されます)。例:
typescript
ここでTaskMetaはidとtitleのみを含みます。doneとdueDateプロパティは除外されています。
Zodでの同等の実装: Zodの.omit()メソッドは、指定されたキーなしのオブジェクトスキーマを生成します。例:
typescript
TaskMetaSchemaはidとtitleのみを含むオブジェクトを検証します。除外されたキー(done、dueDate)は入力に存在する場合でも無視されます。したがって、PickとOmitは、TypeScriptの同等のものと同様に、Zodスキーマをフィールドのサブセットに投影することを可能にします。
TypeScript: Record<Keys, Type>は、キーがKeys型(通常は文字列リテラルのユニオンまたはより一般的なstring/number型)で値がType型のオブジェクト型を構築します。これにより、例えば識別子でインデックス付けされたオブジェクトを記述できます。典型的な例:
typescript
ここで、Record<CatName, CatInfo>はcatsオブジェクトが正確に"miffy"、"boris"、"mordred"のキーを持ち、それぞれがCatInfoに関連付けられていることを保証します。Recordはディクショナリやマップとして使用されるオブジェクトの型付けに非常に便利です。
Zodでの同等の実装: Zodはz.record(keySchema, valueSchema)スキーマを提供して、この種のオブジェクトを検証します。キースキーマ(例えばz.string()やリテラルスキーマ)と値スキーマを使用できます。例:
typescript
ここで、AgeMapSchemaはキーが文字列である限り任意のキーを検証します(デフォルトでは、最初の引数なしのZod z.recordはstringを想定します)。有限のキーセットがある場合は、明示的に名前付きキーを持つz.object({ ... })またはz.enum([...])を使用してキーを制限する方が良いことが多いです。実際、Zodはより厳格なキースキーマを許可します:const CatNameSchema = z.enum(["miffy","boris","mordred"])を定義し、z.record(CatNameSchema, CatInfoSchema)を実行できます – ただし、実行時にJavaScriptオブジェクトのキーは常に文字列であることに注意してください。数値キーでさえ実行時に文字列に変換されます(例えば、キー1はJSオブジェクトで"1"になります)。Zodはこれを考慮し、文字列に変換せずに純粋な数値キースキーマを定義することはできません。
要約すると、Record<Keys, Type>(TypeScript)とz.record(keySchema, valueSchema)(Zod)はどちらもディクショナリの型付けを可能にします。前者は型システムレベルで作用し、後者は実行時にオブジェクトの構造を検証します。
TypeScript: Exclude<U, E>は、ユニオンUから型Eに代入可能なすべてのメンバーを削除して型を作成します。つまり、ユニオンから特定の型を除外します。例えば:
typescript
ここで、Exclude<Role, "admin">はユニオンからリテラル"admin"を削除し、"user" | "guest"のみを残します。別の例:Exclude<string | number | boolean, string>はnumber | booleanを与えます(stringに代入可能なすべてを削除)。これは、望ましくないケースを排除してユニオン型を洗練するのに役立ちます。
Zodでの同等の実装: 実行時にユニオンの可能性を制限するには、通常、除外するケースを含まないスキーマを定義します。例えば、RoleSchema = z.enum(["admin","user","guest"])がある場合、"admin"を含めないサブセットを作成できます。Zodは列挙型に対して.exclude([...])メソッドを提供し、指定された値なしで新しい列挙型スキーマを生成します。例えば:
typescript
このNonAdminSchemaはExclude<Role, "admin">型に正確に対応しますが、実行時の検証も保証します。内部的には、RoleSchema.exclude(["admin"])は単に許可されたリテラルのリストから"admin"を削除します。同様に、Zodは補完的な操作のために.extract([...])を提供します。
TypeScript: Extract<T, U>は、TからUに代入可能なメンバーのみを保持して型を作成します。基本的にExcludeの逆です。例えば:
typescript
別の例:Extract<"a" | "b" | "c", "a" | "f">は"a"を与えます("a"のみが両方に共通するため)。このユーティリティ型は、ユニオンをフィルタリングして特定のサブタイプのみを抽出するために使用されます。
Zodでの同等の実装: Excludeと同様に、Zodは.extract([...])を使用して実行時に列挙型のサブセットに対応するスキーマを取得できます。再びロールの例を取り上げましょう:完全なRoleSchemaから、例えば制限されたロール"user"と"guest"のみを抽出するには:
typescript
ここでLimitedRolesSchemaはExtract<Role, "user" | "guest">型に対応します。実際には、z.enum(["user","guest"])を直接定義することで同じ結果を得ることができますが、.extractは既存のenumからスキーマを導出するのに便利です。.excludeと.extractはZodの列挙型スキーマ(z.enumまたはz.nativeEnum)に適用され、複雑な型の任意のユニオンには適用されないことに注意してください。後者の場合、通常は望ましい新しいユニオンスキーマを手動で構築します。
TypeScript: NonNullable<T>は、Tからnullとundefinedを除外して型を構築します。TypeScriptでは、これらの「nullish」値を含むユニオン型を持つことが一般的で、それらを除外した型が必要になることがあります。例えば:
typescript
ここで、DefinitelyStringはstringのみを含みます(nullもundefinedも含みません)。このユーティリティ型は、コンパイラが型を洗練するのを助けるために、存在条件(if (value != null) { ... })と組み合わせて使用されることが多いです。
Zodでの同等の実装: Zodでは、デフォルトでスキーマはundefinedやnullを受け入れません(指定しない限り)。つまり、z.string()スキーマは文字列のみを受け入れ、undefinedは受け入れません。これらの値を許可するには、明示的にz.string().optional()(undefinedを許可)、z.string().nullable()(nullを許可)、または.nullish()(両方を許可)を使用します。したがって、NonNullable型の同等のものを取得するには、単にスキーマにnull/undefinedを含めないようにします。ただし、より寛容なスキーマから始めて、より厳格にしたい場合は、技術の組み合わせを使用できます:例えば、.unwrap()メソッドを使用して、オプショナルまたはnullableスキーマから基礎となるスキーマを抽出できます。
typescript
ここで、OptionalName.unwrap()は元のz.string()スキーマを返します。これにより、以前に導入されたオプショナルまたはnullableの特性を「削除」できます。別のアプローチとして、null/undefinedを拒否するためのカスタムリファインメントを使用することもできますが、Zodスキーマの動作を考えると冗長になります。
TypeScript: Parameters<T>は、関数型Tのパラメータ型をタプルとして抽出します。これは関数シグネチャを扱う際に特に便利で、別の関数をラップする関数を作成したい場合に役立ちます。例えば:
typescript
ここで、Parameters<typeof greet>はgreetのパラメータを表すタプル[string, number]を抽出します。これにより、safeGreetはgreetとまったく同じパラメータを受け入れることができます。
Zodでの同等の実装: Zodはz.function()を提供して関数スキーマを定義し、パラメータ型と戻り値の型を指定するメソッドを持ちます。Parameters<T>の直接的な同等のものはありませんが、関数スキーマを定義し、TypeScriptのParametersユーティリティを使用して推論された型からパラメータ型を抽出できます:
typescript
ここで、GreetSchemaは文字列と数値を受け取り、文字列を返す関数スキーマを定義します。.implement()メソッドは、スキーマに従って入出力を検証するラッパー関数を作成します。Parameters<GreetFn>を使用してパラメータ型を抽出できます。このアプローチは実行時検証と静的型付けを組み合わせています。
TypeScript: ReturnType<T>は、関数型Tの戻り値の型を抽出します。関数の結果を扱いたいが、その型を明示的に再定義したくない場合に便利です。例えば:
typescript
ここで、ReturnType<typeof createUser>はcreateUserが返すオブジェクト型を抽出します。これにより、構造を複製することなくUser型を定義できます。
Zodでの同等の実装: Parametersと同様に、ReturnTypeの直接的なZodの同等のものはありませんが、Zodの関数スキーマとTypeScriptのReturnTypeユーティリティを組み合わせることができます:
typescript
ここで、CreateUserSchemaで関数スキーマを定義し、その後2つの方法で戻り値の型を抽出します:推論された関数型にTypeScriptのReturnTypeを使用するか、戻り値のスキーマを返すZodの.returnType()メソッドを使用します。どちらのアプローチもUser型を提供します。
TypeScript: InstanceType<T>は、コンストラクタ関数型Tのインスタンス型を抽出します。クラス参照があり、そのクラスのインスタンスを扱いたい場合に便利です。例えば:
typescript
ここで、InstanceType<typeof Point>はPointインスタンスの型を与え、そのプロパティとメソッドを含みます。
Zodでの同等の実装: Zodはクラスインスタンス型の直接的な同等のものを持ちません。Zodはデータの検証に焦点を当てており、クラスの動作には焦点を当てていません。ただし、クラスインスタンスの形状のスキーマを定義できます:
typescript
このアプローチはPointインスタンスの形状に一致するスキーマを定義しますが、完全なクラス関係をキャプチャしません。より複雑なクラス検証には、Zodをclass-validatorや類似のライブラリと組み合わせる必要があるかもしれません。
TypeScriptユーティリティ型とZodスキーマはどちらもデータの信頼性を確保するのに役立ちますが、異なるレベルで作用します:一方はコンパイル時、もう一方は実行時です。
TypeScript: 静的セーフティ(コンパイル時) – ユーティリティ型(および型システム全般)は開発中に動作します。TypeScriptはコードを分析し、コードが実行される前に型の不整合を防ぎます。例えば、Partial<T>は関数がプロパティが欠落しているケースを処理するのを忘れないようにし、ReturnType<Fn>は関数の戻り値の期待される形状を与えます。しかし、これらのチェックはアプリケーションが実行されると存在しなくなります:TypeScriptはJavaScriptへのトランスパイル時に型を消去します。実行時に悪いデータが循環するのを防ぐものは何もありません(JSON、ユーザーフォーム、外部APIなどから)。型システムだけでは実行時の予期しない値から保護できません。
Zod: 動的検証(実行時) – Zodスキーマは実行時にこのギャップを埋めるために介入します。データのスキーマを定義することで、受信したデータが期待される形式と一致することを検証する実行時のガードを作成します。例えば、PartialUserSchemaは実際に受信したオブジェクトが有効なキーのみを含み、その値が正しい型であることを検証します。一方、Partial<User>は単なるコンパイル時の保証です。したがって、ZodとTypeScriptは補完的です:TypeScriptはコードの内部一貫性を確保し(操作するものが宣言された型に対応する)、Zodは外部データが型に準拠することを確保します。
要約すると:
哲学的には、ユーティリティ型は型システムレベルで変換や制約を表現します(開発者を助け、出力コードに直接的な影響はありません)。一方、Zodは実行時レベルで契約を提供します(実行時にデータの検証と変換の具体的な実装を可能にします)。両方のアプローチは異なるが関連するニーズに対応します:コードの堅牢性を維持すること。さらに、ZodはTypeScriptと調和して統合するように設計されています:スキーマから自動的に静的型を推論し、定義の重複を避けます。これは、TypeScriptインターフェースと別のスキーマを定義する代わりに、Zodスキーマ(検証用)を定義し、z.inferを使用して対応する静的TypeScript型を導出できることを意味します – したがって、データ型の単一の信頼できる情報源を持ちます。
結論として、TypeScriptユーティリティ型を知ることで型システムを活用して安全で表現力豊かなコードを書くことができ、Zodをマスターすることで実行時にこれらの型契約を強制するツールを得ることができます。一緒に使用すると、TypeScriptアプリケーションの信頼性が大幅に強化されます:TypeScriptはプログラミングエラーから保護し、Zodは外部世界からの予期しないデータからアプリケーションを保護します。型を作成し、データを検証し、安心してコーディングしましょう 🎉!