TypeScript mapの実務的な使い方|データ変換パターンと実装例
JavaScriptおよびTypeScriptを使った開発では、配列のデータ変換が日常的な業務です。その中でもmapメソッドは最頻出の高階関数の一つです。本記事では、教科書的な説明ではなく、実際のプロジェクトで使用されている実務パターンを中心に解説します。
1. map関数の基本概念
mapは配列の各要素に対して処理を適用し、新しい配列を返すメソッドです。元の配列を変更せず(イミュータブル)、変換後の新しい配列を生成することが特徴です。
基本的なシグネチャは以下の通りです:
array.map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]
TypeScriptでは、入力型Tと出力型Uを明示的に指定できるため、型安全性が保証されます。
2. 業務でのユースケース分析
2-1. APIレスポンスのデータ正規化
最も一般的なユースケースがAPIからのレスポンス処理です。バックエンドから受け取るデータ構造が、フロントエンドで必要な形式と異なることは日常茶飯事です。
例えば、ECサイトのカート機能では、商品マスタAPIから商品IDと価格を取得し、ユーザー情報と組み合わせて表示する必要があります:
// APIレスポンスの型定義
interface ProductApiResponse {
id: string;
name: string;
basePrice: number;
taxRate: number;
stock: number;
createdAt: string;
}
// フロントエンドで必要な型
interface CartItem {
productId: string;
displayName: string;
priceWithTax: number;
isAvailable: boolean;
}
// 実装例:APIレスポンスを正規化
const normalizeProducts = (products: ProductApiResponse[]): CartItem[] => {
return products.map((product) => ({
productId: product.id,
displayName: product.name.toUpperCase(),
priceWithTax: Math.round(product.basePrice * (1 + product.taxRate / 100)),
isAvailable: product.stock > 0,
}));
};
// 使用例
const apiResponse: ProductApiResponse[] = [
{
id: 'prod_001',
name: 'ノートPC',
basePrice: 100000,
taxRate: 10,
stock: 5,
createdAt: '2024-01-15',
},
{
id: 'prod_002',
name: 'マウス',
basePrice: 3000,
taxRate: 10,
stock: 0,
createdAt: '2024-01-16',
},
];
const cartItems = normalizeProducts(apiResponse);
console.log(cartItems);
// 出力:
// [
// { productId: 'prod_001', displayName: 'ノートPC', priceWithTax: 110000, isAvailable: true },
// { productId: 'prod_002', displayName: 'マウス', priceWithTax: 3300, isAvailable: false }
// ]
2-2. ネストされたデータの展開
複数の関連テーブルから取得したデータを統合する場合、mapを使用してネストされた構造を扱います。
// ユーザーと注文履歴の統合データ
interface OrderDetail {
orderId: string;
amount: number;
status: 'pending' | 'completed' | 'cancelled';
}
interface UserWithOrders {
userId: string;
userName: string;
email: string;
orders: OrderDetail[];
}
interface FlattenedUserOrder {
userId: string;
userName: string;
orderId: string;
amount: number;
status: string;
}
// ネストされたデータを平坦化
const flattenUserOrders = (users: UserWithOrders[]): FlattenedUserOrder[] => {
return users.flatMap((user) =>
user.orders.map((order) => ({
userId: user.userId,
userName: user.userName,
orderId: order.orderId,
amount: order.amount,
status: order.status,
}))
);
};
const usersData: UserWithOrders[] = [
{
userId: 'user_001',
userName: '田中太郎',
email: 'tanaka@example.com',
orders: [
{ orderId: 'order_001', amount: 50000, status: 'completed' },
{ orderId: 'order_002', amount: 12000, status: 'pending' },
],
},
{
userId: 'user_002',
userName: '鈴木花子',
email: 'suzuki@example.com',
orders: [
{ orderId: 'order_003', amount: 75000, status: 'completed' },
],
},
];
const flatOrders = flattenUserOrders(usersData);
console.log(flatOrders.length); // 3
3. 実務での複合的な実装パターン
3-1. 条件付き変換とエラーハンドリング
実務では、すべての要素が正常に変換できるとは限りません。エラーハンドリングを含めたロバストな実装が必要です:
interface RawSensorData {
sensorId: string;
rawValue: string; // 数値として不正な値も含まれる可能性
timestamp: number;
}
interface ProcessedSensorData {
sensorId: string;
value: number;
isValid: boolean;
errorMessage?: string;
timestamp: Date;
}
// エラーハンドリング付きのmapパターン
const processSensorData = (
rawData: RawSensorData[]
): ProcessedSensorData[] => {
return rawData.map((data) => {
try {
const parsedValue = parseFloat(data.rawValue);
// バリデーション:センサー値が正常範囲か確認
if (isNaN(parsedValue)) {
return {
sensorId: data.sensorId,
value: 0,
isValid: false,
errorMessage: '数値に変換できません',
timestamp: new Date(data.timestamp),
};
}
if (parsedValue < -40 || parsedValue > 80) {
return {
sensorId: data.sensorId,
value: parsedValue,
isValid: false,
errorMessage: '正常範囲外の値です',
timestamp: new Date(data.timestamp),
};
}
return {
sensorId: data.sensorId,
value: parsedValue,
isValid: true,
timestamp: new Date(data.timestamp),
};
} catch (error) {
return {
sensorId: data.sensorId,
value: 0,
isValid: false,
errorMessage: '予期しないエラーが発生しました',
timestamp: new Date(data.timestamp),
};
}
});
};
const sensorInput: RawSensorData[] = [
{ sensorId: 'sensor_001', rawValue: '25.5', timestamp: 1704067200000 },
{ sensorId: 'sensor_002', rawValue: 'invalid', timestamp: 1704067260000 },
{ sensorId: 'sensor_003', rawValue: '100', timestamp: 1704067320000 },
];
const processedData = processSensorData(sensorInput);
console.log(processedData);
3-2. パフォーマンスを考慮した大規模データ処理
大量データ処理時は、mapの効率を意識する必要があります。オブジェクトの再作成やメモリ使用量に注意が必要です:
interface LogEntry {
level: 'INFO' | 'WARN' | 'ERROR';
message: string;
timestamp: number;
userId?: string;
metadata?: Record;
}
interface AggregatedLog {
level: string;
count: number;
firstOccurrence: Date;
lastOccurrence: Date;
}
// ログエントリを集約(大規模データ用)
const aggregateLogs = (logs: LogEntry[]): Map => {
const aggregated = new Map();
logs.forEach((log) => {
const key = log.level;
const date = new Date(log.timestamp);
if (aggregated.has(key)) {
const existing = aggregated.get(key)!;
existing.count++;
if (date > existing.lastOccurrence) {
existing.lastOccurrence = date;
}
} else {
aggregated.set(key, {
level: log.level,
count: 1,
firstOccurrence: date,
lastOccurrence: date,
});
}
});
return aggregated;
};
// 大規模データのシミュレーション
const generateLogs = (count: number): LogEntry[] => {
const levels: ('INFO' | 'WARN' | 'ERROR')[] = ['INFO', 'WARN', 'ERROR'];
const logs: LogEntry[] = [];
for (let i = 0; i < count; i++) {
logs.push({
level: levels[i % 3],
message: `Log message ${i}`,
timestamp: Date.now() - Math.random() * 86400000,
});
}
return logs;
};
const logs = generateLogs(10000);
const aggregated = aggregateLogs(logs);
console.log(Array.from(aggregated.values()));
4. よくある応用パターン
4-1. mapとfilterの組み合わせ
データ変換と同時にフィルタリングが必要な場合、filterとmapを組み合わせます:
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
}
interface DiscountedProduct {
id: string;
name: string;
originalPrice: number;
discountedPrice: number;
savingAmount: number;
}
// 在庫ありで、かつ価格が高い商品のみ割引適用
const getDiscountedProducts = (
products: Product[],
discountRate: number = 0.1
): DiscountedProduct[] => {
return products
.filter((product) => product.inStock && product.price > 10000)
.map((product) => ({
id: product.id,
name: product.name,
originalPrice: product.price,
discountedPrice: Math.floor(product.price * (1 - discountRate)),
savingAmount: Math.floor(product.price * discountRate),
}));
};
const products: Product[] = [
{
id: 'p001',
name: 'プリンター',
price: 25000,
category: 'electronics',
inStock: true,
},
{
id: 'p002',
name: 'ペン',
price: 500,
category: 'stationery',
inStock: true,
},
{
id: 'p003',
name: 'デスク',
price: 15000,
category: 'furniture',
inStock: false,
},
];
const discounted = getDiscountedProducts(products, 0.15);
console.log(discounted);
4-2. オブジェクトの配列をキーバリューMapへの変換
interface UserProfile {
id: string;
name: string;
email: string;
role: string;
}
// 配列をMapに変換(高速ルックアップ用)
const createUserMap = (users: UserProfile[]): Map => {
return new Map(users.map((user) => [user.id, user]));
};
const users: UserProfile[] = [
{ id: 'u001', name: '山田太郎', email: 'yamada@example.com', role: 'admin' },
{ id: 'u002', name: '田中花子', email: 'tanaka@example.com', role: 'user' },
{ id: 'u003', name: '鈴木次郎', email: 'suzuki@example.com', role: 'user' },
];
const userMap = createUserMap(users);
console.log(userMap.get('u001')?.name); // 山田太郎
4-3. mapを使った依存関係の解決
interface DependencyConfig {
name: string;
version: string;
dependencies?: string[];
}
interface ResolvedDependency {
name: string;
version: string;
resolvedDependencies: DependencyConfig[];
}
// 依存関係を解決するヘルパー関数
const resolveDependencies = (
configs: DependencyConfig[],
configMap: Map
): ResolvedDependency[] => {
return configs.map((config) => ({
name: config.name,
version: config.version,
resolvedDependencies: (config.dependencies || []).map(
(depName) => configMap.get(depName)!
),
}));
};
const depConfigs: DependencyConfig[] = [
{
name: 'express',
version: '4.18.0',
dependencies: ['body-parser'],
},
{
name: 'body-parser',
version: '1.20.0',
dependencies: [],
},
{
name: 'lodash',
version: '4.17.21',
dependencies: [],
},
];
const configMap = new Map(depConfigs.map((c) => [c.name, c]));
const resolved = resolveDependencies(depConfigs, configMap);
console.log(resolved);
5. TypeScript特有の注意点と最適化
5-1. 型推論とジェネリクス
TypeScriptは通常、mapの戻り値の型を推論できますが、複雑な変換では明示的に型を指定することが推奨されます:
// 明示的な型指定で型安全性を確保
const transformData = <T, U>(
items: T[],
transformer: (item: T) => U
): U[] => {
return items.map(transformer);
};
interface Source {
id: number;
value: string;
}
interface Target {
identifier: number;
content: string;
}
const sources: Source[] = [
{ id: 1, value: 'hello' },
{ id: 2, value: 'world' },
];
// 型安全な変換
const targets = transformData<Source, Target>(sources, (source) => ({
identifier: source.id,
content: source.value.toUpperCase(),
}));
console.log(targets); // Type: Target[]
5-2. Partial型とオプショナルプロパティの処理
interface FullUser {
id: string;
name: string;
email: string;
phone: string;
address: string;
}
type PartialUser = Partial;
// 部分的なデータを完全な形に変換
const fillMissingFields = (
partialUsers: PartialUser[],
defaults: FullUser
): FullUser[] => {
return partialUsers.map((user) => ({
id: user.id ?? defaults.id,
name: user.name ?? defaults.name,
email: user.email ?? defaults.email,
phone: user.phone ?? defaults.phone,
address: user.address ?? defaults.address,
}));
};
const incomplete: PartialUser[] = [
{ id: 'u001', name: '太郎' },
{ id: 'u002', email: 'email@example.com' },
];
const defaults: FullUser = {
id: 'default',
name: '名義人',
email: 'default@example.com',
phone: '000-0000-0000',
address: '東京都',
};
const complete = fillMissingFields(incomplete, defaults);
5-3. 非同期操作との組み合わせ
実務では、非同期処理を伴うデータ変換が頻繁に発生します。この場合、Promise配列を扱う必要があります:
interface RemoteResource {
id: string;
url: string;
}
interface CachedResource {
id: string;
content: string;
fetchedAt: Date;
}
// 非同期変換:複数リソースを並列取得
const fetchAndCacheResources = async (
resources: RemoteResource[]
): Promise => {
// 全リクエストを並列実行
const promises = resources.map(async (resource) => {
try {
// 実際の実装ではfetchやaxiosを使用
const response = await fetch(resource.url);
const content = await response.text();
return {
id: resource.id,
content: content,
fetchedAt: new Date(),
};
} catch (error) {
console.error(`Failed to fetch ${resource.id}:`, error);
return {
id: resource.id,
content: '',
fetchedAt: new Date(),
};
}
});
return Promise.all(promises);
};
// 使用例
const resources: RemoteResource[] = [
{ id: 'r001', url: 'https://example.com/data1' },
{ id: 'r002', url: 'https://example.com/data2' },
];
// await fetchAndCacheResources(resources);
6. パフォーマンスに関する注意点
6-1. 大規模配列でのメモリ効率
非常に大規模なデータセット(数百万件以上)では、mapが全体をメモリに展開するため、ジェネレータやストリーム処理の検討が必要です:
// ジェネレータを使った遅延評価
function* mapLazy<T, U>(
items: T[],
transformer: (item: T) => U
): Generator<U> {
for (const item of items) {
yield transformer(item);
}
}
// 使用例
const numbers = Array.from({ length: 1000000 }, (_, i) => i);
const doubled = mapLazy(numbers, (n) => n * 2);
// 必要な分だけ処理
let count = 0;
for (const value of doubled) {
if (count++ >= 10) break;
console.log(value);
}
6-2. 過度なネストの回避
mapを複数回ネストすると、処理が複雑になりパフォーマンスも低下します:
// 非効率な例:複数回のmap
const inefficient = data
.map((x) => x.value)
.map((y) => y * 2)
.map((z) => z + 10);
// 効率的な例:単一のmapで統合
const efficient = data.map((x) => x.value * 2 + 10);
7. デバッグとテスト
実務では、mapを使った変換結果をテストすることは重要です:
// テスト例(Jest)
describe('データ変換テスト', () => {
test('normalizeProducts は正しく価格を計算する', () => {
const input: ProductApiResponse[] = [
{
id: 'prod_001',
name: 'テスト商品',
basePrice: 1000,
taxRate: 10,
stock: 5,
createdAt: '2024-01-01',
},
];
const result = normalizeProducts(input);
expect(result).toHaveLength(1);
expect(result[0].priceWithTax).toBe(1100);
expect(result[0].isAvailable).toBe(true);
});
test('在庫なし商品は isAvailable が false', () => {
const input: ProductApiResponse[] = [
{
id: 'prod_002',
name: '在庫なし',
basePrice: 5000,
taxRate: 10,
stock: 0,
createdAt: '2024-01-01',
},
];
const result = normalizeProducts(input);
expect(result[0].isAvailable).toBe(false);
});
});
まとめ
TypeScriptのmapメソッドは、単なる配列変換ツールではなく、実務におけるデータ処理パイプラインの中核をなします。重要なポイントは以下の通りです:
- 型安全性: TypeScriptの型推論を活用し、変換前後のデータ構造を明確に定義する
- イミュータビリティ: 元データを変更せず、新しい配列を生成する原則を守る
- エラーハンドリング: 本番環境では、予期しないデータにも対応する実装が必須
- パフォーマンス意識: 大規模データ処理時は、メモリ効率とアルゴリズムの効率を検討する
- 組み合わせの工夫:
filter、reduceなどと組み合わせ、複雑な処理を簡潔に記述する
これらのパターンを習得することで、保守性が高く、スケーラブルなTypeScriptコードを書くことができます。

