TypeScriptは基本的に、コードを書いている段階で間違いを見つけてくれる優秀な子です。
......が、プログラムが実際に動いているときに「本当にこのデータは期待通りの形なの?」と不安になることがあります。
(特に、外部のAPIからデータを取ってきたときなど)
そんなときに役立つのが 「ユーザー定義型ガード関数」 です。
1. まず型の意義から
JavaScriptで開発をしていると、時々、意図しない値が変数に入ってしまうことがあります。
// JavaScriptの例
let message = "こんにちは";
// ... いろんな処理 ...
message = 123; // あれ?文字列のはずが数値になっちゃった!エラーにならないよ~
console.log(message.toUpperCase()); // エラー!
TypeScriptは、変数に入れるデータの種類(型)をあらかじめ決めておくことで、このような間違いをコードを書いている段階で教えてくれます。
// TypeScriptの例
let message: string = "こんにちは";
// message = 123; // ここでTypeScriptがエラーを出す!「型 'number' を型 'string' に割り当てることはできません。」
console.log(message.toUpperCase()); // 安全!
このようにReact+TypeScriptの開発では、コンポーネント間で受け渡しするデータ(Props)や、コンポーネントが内部で持つ状態(State)、APIから取得するデータなどに型を付けることで、バグを減らし、開発しやすくなります。
2. 型ガードって何?
TypeScriptはコードを書いているときは賢いですが、プログラムが実行されているとき(ランタイム)には、外部から来たデータ(例えば、Web APIから送られてきたJSONデータ)が本当に約束通りの「型」なのか、100%確信は持てません。
そこで登場するのが型ガード (Type Guard) です。
型ガードは、「この変数は、本当に〇〇型ですか?」と実行中にチェックしてくれる仕組みです。
そして、チェックがOKだったら、その後の処理では「うん、これは確かに〇〇型だね!」とTypeScriptが認識してくれます。
TypeScriptには、typeof
という簡単な型ガードが用意されています。
function printLength(value: string | number) {
// valueは string か number のどちらか
if (typeof value === 'string') {
// typeof でチェック!
// この if の中では、valueは間違いなく string 型だとわかる
console.log("文字列の長さ:", value.length);
} else {
// この else の中では、valueは number 型だとわかる
console.log("数値:", value);
}
}
printLength("Hello"); // 出力: 文字列の長さ: 5
printLength(100); // 出力: 数値: 100
3. ユーザー定義型ガード とは「自分で作る」型ガード
typeof
は便利ですが、文字列や数値のような基本的な型しかチェックできません。
私たちが自分で定義した複雑なオブジェクトの型(interface
やtype
で作ったもの)をチェックしたい場合はどうすればいいでしょうか?
例えば、ユーザー情報を表す型を考えます。
interface User {
id: number;
name: string;
isAdmin: boolean;
}
// APIから取得したデータ (最初は型がはっきりしないことが多い)
const apiData: any = { id: 1, name: "Tanaka", isAdmin: true };
// const apiData: any = { id: 2, name: "Suzuki" }; // isAdminがないかもしれない…
// const apiData: any = { userId: 3, userName: "Sato" }; // プロパティ名が違うかもしれない…
// この apiData が本当に User 型かチェックしたい!
// でも typeof apiData === 'User' なんて書き方はできない…
こんなときに、「このデータが User
型の条件を満たしているか?」をチェックする専用の関数を作ります。
これがユーザー定義型ガード関数です。
4. 作り方
3つのステップで見ていきましょう。
1. 宣言
普通の関数と似ていますが、戻り値の書き方が特別です。
// 引数名 チェックしたい型
// ↓ ↓
function isUser(data: any): data is User {
// 中身は後で書く
}
-
function isUser(data: any)
- 関数名はわかりやすいものに(
is
+ 型名 が一般的)。 - 引数
data
は、チェックしたいデータを受け取ります。 - 型は
any
やunknown
(まだ型が不明なデータ)にしておくことが多いです。
- 関数名はわかりやすいものに(
-
: data is User
- これは「型述語 (Type Predicate)」と呼ばれます。
- 「もしこの
isUser
関数がtrue
を返したら、引数のdata
はUser
型だと保証しますよ!」とTypeScriptに伝えるための特別な書き方です。
2. 中身
関数の中では、引数 data
が User
型であるための条件をチェックし、最終的に true
か false
を返します。
さて...、User
型である条件はなんでしたっけ?
-
id
プロパティがあって、その型がnumber
である -
name
プロパティがあって、その型がstring
である -
isAdmin
プロパティがあって、その型がboolean
である
これをコードで書くと、次のようになります。
interface User {
id: number;
name: string;
isAdmin: boolean;
}
function isUser(data: any): data is User {
// 1. まず、dataがオブジェクトであるか、nullでないかを確認
if (typeof data !== 'object' || data === null) {
return false; // オブジェクトじゃなければUser型ではない
}
// 2. 必要なプロパティが存在し、かつ型が正しいかチェック
const hasId = typeof data.id === 'number';
const hasName = typeof data.name === 'string';
const hasIsAdmin = typeof data.isAdmin === 'boolean';
// 3. すべての条件を満たしていれば true を返す
return hasId && hasName && hasIsAdmin;
}
注意:data.id
などにアクセスする前に data
がオブジェクトであることを確認するのがより安全です。
3. 使い方
作った型ガード関数は、主に if
文の中で使います。
const apiData: any = fetchUserDataFromAPI(); // APIからデータを取ってきた想定 (型は不明)
if (isUser(apiData)) {
// --- isUser関数が true を返した場合 ---
// このブロックの中では、TypeScriptは apiData が User 型だと認識してくれる!
console.log("ようこそ、", apiData.name, "さん!"); // 安全に .name にアクセスできる
console.log("ID:", apiData.id); // .id もOK
if (apiData.isAdmin) { // .isAdmin もOK
console.log("管理者権限があります。");
}
} else {
// --- isUser関数が false を返した場合 ---
// このブロックの中では、apiData は User 型ではないことがわかる
console.error("取得したデータはユーザー情報ではありませんでした。", apiData);
}
型ガード関数を使うことで、if
文の中では apiData
が安全に User
型として扱え、プロパティ(.id
, .name
, .isAdmin
)にアクセスしてもTypeScriptがエラーを出さなくなります。
VSCodeなどのエディタの入力補完も効くようになります。(便利...。)
5. Reactでの実践
さて、実際の開発現場ではどんな場面で使うのでしょう?
場面1:APIから取得したデータ
最も一般的な使い方です。
useEffect
フックの中でAPIを呼び出し、取得したデータを useState
で状態として保持する前などに型ガードでチェックします。
import React, { useState, useEffect } from 'react';
interface Product {
id: string;
name: string;
price: number;
}
// Product型の型ガード関数
function isProduct(data: any): data is Product {
return (
typeof data === 'object' &&
data !== null &&
typeof data.id === 'string' &&
typeof data.name === 'string' &&
typeof data.price === 'number'
);
}
// 配列全体がProductの配列かチェックする型ガード
function isProductArray(data: any): data is Product[] {
// Array.isArrayで配列かチェックし、everyですべての要素がisProductを満たすかチェック
return Array.isArray(data) && data.every(item => isProduct(item));
}
function ProductList() {
const [products, setProducts] = useState<Product[]>([]); // 商品リストの状態 (最初は空)
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
fetch('/api/products') // APIから商品リストを取得
.then(response => {
if (!response.ok) throw new Error('データの取得に失敗しました');
return response.json(); // JSONに変換 (この時点では型は any や unknown)
})
.then(data => {
// 型ガードでチェック!
if (isProductArray(data)) {
// チェックOK! 安全に Product[] 型として State を更新
setProducts(data);
setError(null); // エラーがあればクリア
} else {
// チェックNG! 予期しないデータ形式だった
console.error("取得したデータ形式が不正です:", data);
throw new Error('サーバーから予期しない形式のデータが返されました。');
}
})
.catch(err => {
// エラーハンドリング
setError(err.message);
setProducts([]); // エラー時はリストを空にする
})
.finally(() => {
setLoading(false); // ローディング完了
});
}, []); // 最初の一回だけ実行
if (loading) return <p>読み込み中...</p>;
if (error) return <p style={{ color: 'red' }}>エラー: {error}</p>;
return (
<div>
<h2>商品リスト</h2>
<ul>
{/* ここでは products は Product[] 型であることが保証されている */}
{products.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
</li>
))}
</ul>
</div>
);
}
export default ProductList;
このように、APIレスポンスを setState
する前に型ガードでチェックすることで、「undefined
のプロパティ name
を読み取れません」のような実行時エラーを防ぐことができます。
場面2:コンポーネントが複数の型のデータを受け取る場合
コンポーネントのPropsやStateが複数の可能性を持つ型(Union Type)の場合、型ガードを使ってどの型なのかを判別し、表示や処理を分けることができます。
type TextMessage = { kind: "text"; text: string };
type ImageMessage = { kind: "image"; imageUrl: string; altText: string };
type Message = TextMessage | ImageMessage; // MessageはTextMessageかImageMessageのどちらか
// TextMessageの型ガード
function isTextMessage(message: Message): message is TextMessage {
return message.kind === "text";
}
// ImageMessageの型ガード (isTextMessageがfalseならこれ、という場合も多い)
function isImageMessage(message: Message): message is ImageMessage {
return message.kind === "image";
}
function MessageItem({ message }: { message: Message }) {
if (isTextMessage(message)) {
// message は TextMessage 型
return <p>{message.text}</p>;
} else if (isImageMessage(message)) {
// message は ImageMessage 型
return <img src={message.imageUrl} alt={message.altText} style={{ maxWidth: '100%' }} />;
} else {
// 通常ここには来ないはずだが、念のため
return <p>不明なメッセージタイプです</p>;
}
}
// --- 使い方 ---
function ChatView() {
const messages: Message[] = [
{ kind: "text", text: "こんにちは!" },
{ kind: "image", imageUrl: "/images/cat.jpg", altText: "かわいい猫" },
{ kind: "text", text: "元気ですか?" },
];
return (
<div>
{messages.map((msg, index) => (
<MessageItem key={index} message={msg} />
))}
</div>
);
}
この例のように kind
プロパティを使って型を区別する方法は「判別可能なUnion型 (Discriminated Unions)」と呼ばれ、TypeScriptが if (message.kind === 'text')
のような単純なチェックだけでも型を絞り込んでくれることが多いです。
しかし、型ガード関数として明示的に定義することも可能です。
6.【応用編】Zodによる型ガード
ここまで、自分で条件を記述するユーザー定義型ガード関数 (A is B
) の基本を見てきました。
これらは非常に便利ですが......、扱う型が複雑になったり、ネストしたオブジェクトになったりすると、型ガード関数の実装が長くなりがちです。
また、プロパティの存在チェックや基本的な型チェックだけでなく、もっと細かいバリデーション(例えば、文字列が空でないか、数値が特定の範囲内かなど)を行いたくなることもあります。
さらに、TypeScriptの型定義 (interface
や type
) と、実行時のバリデーションロジック(型ガード関数の中身)を別々に書く必要があり、両者の同期を取り忘れるとバグの原因になりかねません。
これらの課題を解決し、型ガードをより宣言的で安全、かつシンプルに記述できるライブラリとして Zod が注目されています。
Zodを使った型ガードの実装
先ほどの User
型の例を Zod を使って書き換えてみましょう。
1. Zodスキーマの定義
まず、User
型の「あるべき姿」を Zod のスキーマとして定義します。
import { z } from 'zod'; // Zodライブラリをインポート
// Userのスキーマを定義
const userSchema = z.object({
id: z.number().int().positive(), // number型、整数、かつ正の数
name: z.string().min(1), // string型、かつ1文字以上
isAdmin: z.boolean(), // boolean型
email: z.string().email().optional(), // string型、email形式、存在しなくてもOK (optional)
});
// (参考) ネストしたオブジェクトや配列も定義可能
const postSchema = z.object({
postId: z.string().uuid(), // UUID形式の文字列
title: z.string(),
tags: z.array(z.string()), // 文字列の配列
});
z.object()
でオブジェクトの形を、z.string()
や z.number()
などで各プロパティの型を指定します。
.int()
や .positive()
、.min(1)
、.email()
、.optional()
のように、より詳細なバリデーションルールをメソッドチェーンで簡単に追加できるのが Zod の強力な点です。
2. スキーマからTypeScriptの型を生成
Zod スキーマがあれば、z.infer
を使って対応する TypeScript の型を自動生成できます。
これにより、interface User
を手動で定義する必要がなくなり、スキーマと型の定義が常に同期します。
type User = z.infer<typeof userSchema>;
/*
// 上記は以下のように手動で書いた型定義とほぼ同等
interface User {
id: number;
name: string;
isAdmin: boolean;
email?: string | undefined; // optional なので ? が付く
}
*/
3. Zodを使った型ガード関数
Zod スキーマには、データを検証するためのメソッドが用意されています。
-
schema.parse(data)
- データがスキーマに合致すれば、型付けされたデータを返します。
- 合致しなければ、エラーをthrowします。
-
schema.safeParse(data)
- エラーをスローせず、検証結果をオブジェクト (
{ success: boolean, data?, error? }
) で返します。 -
success
がtrue
ならdata
に型付けされたデータが、false
ならerror
に詳細なエラー情報が入ります。
- エラーをスローせず、検証結果をオブジェクト (
この safeParse
を使うと、ユーザー定義型ガード関数を非常にシンプルに書けます。
function isUserWithZod(data: unknown): data is User {
// safeParse を呼び出し、結果の success プロパティを返すだけ
const result = userSchema.safeParse(data);
return result.success;
}
// --- 使い方 ---
const apiData: unknown = fetchUserDataFromAPI(); // 型は不明 (unknown)
if (isUserWithZod(apiData)) {
// apiData は User 型として安全に扱える
console.log("ようこそ、", apiData.name, "さん!");
} else {
// データが User 型のスキーマを満たさなかった場合
console.error("取得したデータはユーザー情報ではありませんでした。");
// エラー詳細を知りたい場合は、再度 safeParse するか、
// 最初の safeParse の結果を保持しておく
const validationResult = userSchema.safeParse(apiData);
if (!validationResult.success) {
console.error("バリデーションエラー:", validationResult.error.flatten());
/* 例:
バリデーションエラー: {
fieldErrors: {
id: [ 'Expected number, received string' ],
name: [ 'String must contain at least 1 character(s)' ]
},
formErrors: []
}
*/
}
}
手書きの型ガード関数にあったような、typeof
によるチェックやプロパティの存在確認 ('id' in data
など) のロジックを自分で書く必要がなくなりました。
スキーマ定義がそのままバリデーションルールとなり、safeParse
を呼ぶだけで済むため、コードが非常に簡潔になり、バグも混入しにくくなります。
Reactでの実践 (Zod版)
「5. Reactでの実践」で見たAPIから商品リストを取得する例も、Zod を使って書き換えることができます。
import React, { useState, useEffect } from 'react';
import { z } from 'zod';
// 1. Productのスキーマを定義
const productSchema = z.object({
id: z.string().uuid(), // IDはUUID形式とする
name: z.string().min(1),
price: z.number().positive(), // 価格は正の数
});
// 2. Product配列のスキーマを定義
const productArraySchema = z.array(productSchema);
// 3. スキーマからProduct型を推論
type Product = z.infer<typeof productSchema>;
function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
setError(null); // 開始時にエラーをリセット
fetch('/api/products')
.then(response => {
if (!response.ok) throw new Error('データの取得に失敗しました');
return response.json();
})
.then(data => {
// Zod の safeParse でバリデーション!
const result = productArraySchema.safeParse(data);
if (result.success) {
// バリデーション成功! result.data に型付けされた Product[] が入っている
setProducts(result.data);
} else {
// バリデーション失敗!
console.error("APIレスポンスの形式が不正です:", result.error.flatten());
// ユーザー向けのエラーメッセージを設定
// result.error.message から簡単なエラー文字列も取得可能
throw new Error('サーバーから予期しない形式のデータが返されました。');
}
})
.catch(err => {
setError(err.message || '不明なエラーが発生しました');
setProducts([]); // エラー時はリストを空にする
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) return <p>読み込み中...</p>;
if (error) return <p style={{ color: 'red' }}>エラー: {error}</p>;
return (
<div>
<h2>商品リスト</h2>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
</li>
))}
</ul>
</div>
);
}
export default ProductList;
手書きの isProduct
や isProductArray
関数が不要になり、代わりに Zod のスキーマ productArraySchema
と safeParse
を使ってバリデーションを行っています。
safeParse
の結果 (result.success
) で処理を分岐し、成功すれば result.data
を使って状態を更新、失敗すれば result.error
を使ってエラーの詳細を確認できます。
これにより、APIレスポンスの検証がより堅牢かつ簡潔になりましたね。
7. まとめ
お疲れ様でした!ユーザー定義型ガードの基本から、Zodを使った応用的なデータ検証まで見てきました。
TypeScriptの型システムはコンパイル時に多くのエラーを防いでくれますが、APIレスポンスのような実行時にやってくる未知のデータに対しては、型ガードやZodのようなバリデーションライブラリを使って「本当に期待通りのデータか?」を確認することが、堅牢なアプリケーションを作る上で非常に重要です。
この記事が、みなさんのより安全でメンテナンスしやすいReact + TypeScriptアプリケーション開発の一助になれば幸いです🐱
参考文献