変換器はサンプル値から下書きを生成します。以下の手動調整には API 契約の理解が必要です。推論された型は実行時のデータを検証しません。
基本から始めましょう。JSONオブジェクトが与えられた場合、どのようにTypeScriptインターフェースに変換するのでしょうか?
APIからの簡単なユーザーレスポンスです:
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"isActive": true
}
対応するTypeScriptインターフェース:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
各JSON値はTypeScriptのプリミティブにマッピングされます:number、string、または boolean。オブジェクトキーはプロパティ名になります。
実際のAPIがフラットな構造を返すことはほとんどありません。住所を持つユーザーを考えてみましょう:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
ネストされたオブジェクトを独立したインターフェースに分割します:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
これにより各型が焦点を絞り、再利用可能になります。別のエンドポイントも Address を返す場合、同じインターフェースを使用できます。
フィールドに配列が含まれている場合、要素の型を注釈します:
{
"id": 42,
"username": "jdoe",
"roles": ["admin", "editor"],
"posts": [
{ "id": 1, "title": "Hello World" },
{ "id": 2, "title": "TypeScript Tips" }
]
}
interface Post {
id: number;
title: string;
}
interface User {
id: number;
username: string;
roles: string[];
posts: Post[];
}
シンプルなケースでは Type[] 構文を使用します。複雑なジェネリクスの場合、Array<Type> の方が明確かもしれませんが、どちらを選ぶかよりも一貫性の方が重要です。
APIは存在するかもしれないし、しないかもしれないフィールドを返すことがよくあります。? でマークします:
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // レスポンスに存在しない場合がある
}
オプショナルフィールド(bio?: string)は、キー自体が完全に存在しない可能性があることを意味します。これは、キーは存在するが値が null である可能性があるnullableフィールドとは異なります。
本番APIは乱雑です。一般的な問題の対処法は以下の通りです。
フィールドが明示的に null になり得る場合:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // ISO日付文字列またはnull
}
フィールドが不在とnullの両方になり得る場合、2つを組み合わせます:
interface User {
id: number;
username: string;
middleName?: string | null; // 存在しないか、存在するがnullの可能性
}
一部のフィールドは複数の型を受け入れます。検索APIが混合結果を返す場合があります:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
判別共用体でモデリングします:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
type フィールドが判別子として機能します。TypeScriptはチェック時に自動的に型を絞り込みます:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScriptはこれがUserResultであることを知っている
} else {
console.log(result.title); // TypeScriptはこれがPostResultであることを知っている
}
}
APIが固定の文字列値セットを返す場合、TypeScriptのenumの代わりに文字列リテラルユニオンを使用します:
{ "status": "active" }
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
文字列リテラルユニオンはよりシンプルで、ツリーシェイキングが可能で、enum のようにランタイムJavaScriptを生成しません。逆マッピング(数値から名前)が必要な場合にのみ enum を使用してください。
interface と type の両方がオブジェクトの形状を記述できます。それぞれをいつ選ぶかは以下の通りです:
拡張する可能性のあるオブジェクト形状には interface を使用:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
ユニオン、インターセクション、計算された型には type を使用:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
実際には、ほとんどの場合どちらでも機能します。プロジェクトで1つの規約を選び、それに従いましょう。多くのチームがデータモデルに interface、それ以外すべてに type を使用しています。
最大の安全性のために、Zodで形状を定義し、そこからTypeScript型を推論します:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
bio: z.string().nullable().optional(),
});
type User = z.infer<typeof UserSchema>;
// ランタイムでバリデーション
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // 形状が一致しない場合はエラーをスロー
Zodを使えば、型とバリデーションロジックが1か所に集まります。APIが形状を変更した場合、parse() は不正なデータがアプリ全体に伝播するのではなく、即座にエラーをスローします。
型はデータがアプリケーションに入る境界で適用してこそ有用です。
型を強制する汎用fetch関数を作成します:
async function fetchJson<T>(url: string, schema?: z.ZodType<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (schema) {
return schema.parse(data);
}
return data as T;
}
// 使用法
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Axiosを使用する場合、レスポンスジェネリクスに型を適用します:
import axios from 'axios';
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
perPage: number;
lastPage: number;
};
}
async function getUsers(page: number = 1) {
const response = await axios.get<PaginatedResponse<User>>('/api/users', {
params: { page },
});
return response.data; // 完全に型付き
}
Reactアプリでは、データフェッチライブラリと型を組み合わせます:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// コンポーネント内で
const { data: user, isLoading } = useUser(42);
// userはUser | undefinedとして型付けされる
tsconfig.json で strict モードを有効にします(少なくとも strictNullChecks):
{
"compilerOptions": {
"strict": true
}
}
strictNullChecksがなければ、TypeScriptは潜在的に null な値で .username にアクセスしても警告しません。これではAPIレスポンスを型付けする意味がなくなります。
readonly を使用APIレスポンスは受け取ったデータであり、変更すべきデータではありません。プロパティを readonly でマークします:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
またはインターフェース全体にユーティリティ型を使用します:
type ImmutableUser = Readonly<User>;
これにより、微妙なバグを引き起こす偶発的な変更を防ぎます。
ブランド型で異なるエンティティのID混同を防ぎます:
type UserId = number & { readonly __brand: 'UserId' };
type PostId = number & { readonly __brand: 'PostId' };
function getUser(id: UserId): Promise<User> { /* ... */ }
function getPost(id: PostId): Promise<Post> { /* ... */ }
const userId = 42 as UserId;
const postId = 7 as PostId;
getUser(userId); // OK
getUser(postId); // 型エラー!UserIdが必要な場所にPostIdを渡すことはできません
すべてのAPI関連型を専用ディレクトリに保管します:
src/
types/
api/
user.ts
post.ts
common.ts // PaginatedResponseなどの共有型
index.ts // 再エクスポート
これにより型の検索、インポート、メンテナンスが容易になります。APIが変更された場合、1つのファイルを更新すれば、コンパイラが壊れるすべての箇所を表示します。
外部データを決して信頼しないでください。完璧な型があっても、APIが予期しないものを返す可能性があります。レスポンスがアプリに入る場所でバリデーションを行います:
// こうしないでください
const user = (await response.json()) as User; // 盲目的に信頼
// こうしてください
const data = await response.json();
const user = UserSchema.parse(data); // 形状をバリデーション
型アサーション(as User)はコンパイラを黙らせますが、データをチェックしません。ランタイムバリデーション(Zod、io-ts、Valibot)は実際に形状が一致するかを検証します。
大規模または複雑なJSONペイロードを扱う場合、型を書く前に JSONエディター を使って構造を探索し理解してください。生テキストよりも適切なエディターでネストされた関係を見る方がはるかに簡単です。
JSON APIを扱うチームのための関連ガイド:
APIにOpenAPI(Swagger)仕様がある場合、JSON変換を完全にスキップしてください。openapi-typescript などのツールが仕様から直接型を生成します:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
このアプローチが優れている理由は、仕様がオプショナルフィールド、nullable型、enumを明示的に定義しているからです。サンプルデータから推測することなく正確な型を取得できます。
処理はブラウザーで行われます。保存と共有は任意で、アカウントが必要です。選択した内容が当サービスに送信されます。
はい。コンバーターは各ネストされたオブジェクトに対して別々のインターフェイスを生成し、適切に参照します。
生成されたTypeScriptの型はプロダクション対応です。コピーするか、.tsファイルとしてダウンロードしてください。
はい。コンバーターは配列型を検出し、適切なTypeScript配列構文(Type[])を生成します。値がnullまたはundefinedの場合、オプションフィールドが検出されます。