【無料配布】10日間で学べるTypeScript学習資料

【TypeScript】Array.map/Mapオブジェクト/Mapped Typesを解説

TypeScriptを学習し始めると、至る所で「Map(マップ)」という言葉に出会います。

しかし、文脈によってその意味は全く異なります。

3つのMap
  • Array.map: 配列の各要素を変換して新しい配列を作るメソッド
  • Mapオブジェクト: キーと値のペアを保持するデータ構造
  • Mapped Types: 既存の型から新しい型を生成する型システムの機能

この記事では、これら3つの「Map」について、初心者からシニアエンジニアまでが納得できるレベルまで深く、かつ網羅的に解説します。

目次

【配列編】Array.prototype.map() の基礎

mapメソッドは最も頻繁に使われるツールの一つです。

TypeScript環境下では、この mapメソッドが「どの型の値を受け取り、どの型の新しい配列を返すか」をコンパイラが常に監視してくれます。

ランタイムでの予期せぬエラー(例えば、存在しないプロパティへのアクセスなど)を劇的に減らすことができます。

本章では、TypeScriptにおける Array.map の挙動を、型システムの観点から徹底的に深掘りします。

Array.mapの基本構文と型推論

TypeScriptの map は、元の配列の要素を一つずつ取り出し、変換処理を施した後に「新しい配列」を生成します。

このとき、TypeScriptの強力な「型推論(Type Inference)」が働きます。

推論の仕組み

元の配列が number[] 型であれば、map のコールバック関数の第1引数は自動的に number 型であると推論されます。

また、コールバック関数が返す値が string 型であれば、最終的な戻り値の配列は string[] 型になります。

const prices = [100, 200, 300]; // number[] と推論される

const priceLabels = prices.map((price) => {
  // price は自動的に number 型になる
  return `¥${price.toLocaleString()}`; 
});

// priceLabels は自動的に string[] 型になる

インデックスと配列全体の型

map のコールバックには、第2引数に index(数値)、第3引数に array(元の配列そのもの)が渡されます。

これらもすべて型定義されており、誤って index を文字列として扱おうとするとコンパイルエラーが発生します。

ジェネリクスを用いた戻り値の型定義

基本的には自動推論に任せて問題ありません。

ただ、大規模な開発や複雑なオブジェクトの変換を行う際には、「ジェネリクス(Generics)」を用いて戻り値を明示的に制御する手法が重要になります。

インターフェース間の変換

例えば、APIから取得した「生データ(Raw Data)」を、画面表示用の「ビューモデル(View Model)」に変換する場合を考えてみましょう。

interface ApiUser {
  user_id: string;
  first_name: string;
  last_name: string;
  age: number;
}

interface UserCard {
  id: string;
  fullName: string;
  isAdult: boolean;
}

const apiUsers: ApiUser[] = [
  { user_id: "001", first_name: "Taro", last_name: "Yamada", age: 25 }
];

// ジェネリクス <UserCard> を指定することで、戻り値が正しいか厳格にチェックされる
const userCards = apiUsers.map<UserCard>((user) => ({
  id: user.user_id,
  fullName: `${user.first_name} ${user.last_name}`,
  isAdult: user.age >= 20
}));

なぜ明示的な指定が必要か?

ジェネリクスを指定しない場合、コールバック関数の中でプロパティ名を書き間違えても、その間違った形のまま型推論されてしまいます。

ジェネリクスで期待する型を指定しておくことで、「変換後のオブジェクトが UserCard の仕様を満たしていない」場合に、即座にコンパイルエラーとして検知できるようになります。

これが、大規模開発における「堅牢なコード」の正体です。

React/Next.jsでのJSX出力とmapの鉄板パターン

フロントエンド開発、特に React や Next.js において、map はリストレンダリングの「標準」です。

しかし、ここで型安全性を疎かにすると、バグだけでなくパフォーマンスの低下も招きます。

正確なPropsの受け渡し

map 内でコンポーネントを呼び出す際、渡すプロパティ(Props)がコンポーネント側の型定義と一致しているかを TypeScript がチェックします。

const UserList: React.FC<{ users: ApiUser[] }> = ({ users }) => {
  return (
    <ul>
      {users.map((user) => (
        // UserItemコンポーネントに渡す型が正しいか常に検証される
        <UserItem key={user.user_id} name={user.first_name} />
      ))}
    </ul>
  );
};

keyプロパティの重要性と型の関係

Reactの map 処理で避けて通れないのが key の設定です。

スクロールできます
項目index を key にする一意識別子(ID)を key にする
numberstring / number
再レンダリング非効率(順序変更に弱い)高効率(変更箇所のみ更新)
推奨度低(静的なリストのみ可)高(標準パターン)

条件付きレンダリングとの組み合わせ

リストの中に「特定の条件を満たす場合のみ表示したい」要素がある場合、map の前に filter を挟むか、map 内で三項演算子を使用します。

// 20歳以上のユーザーだけを表示する型安全な実装
{users
  .filter((user) => user.age >= 20)
  .map((user) => (
    <li key={user.user_id}>{user.first_name}</li>
  ))}

【データ構造編】Mapオブジェクトの基本と応用

配列の map メソッドと混同されやすいですが、JavaScript(ES6以降)およびTypeScriptには、「Map」という名前の独立したデータ構造が存在します。

多くの開発者は、キーと値のペアを管理する際に {}(オブジェクトリテラル)を使いがちです。

TypeScriptにおいて Map オブジェクトを使いこなすことは、パフォーマンスの向上だけでなく、「型安全性の強化」に直結します。

本章では、オブジェクトリテラルとの違いから、TypeScriptならではの型定義術までを詳しく解説します。

Mapオブジェクト vs Objectリテラル

「キーと値でデータを持ちたいなら {} で十分じゃないの?」という疑問は、TypeScriptを触り始めた誰もが抱くものです。

しかし、Map にはオブジェクトリテラルにはない強力な特性が備わっています。

スクロールできます
特徴Objectリテラル ({})Mapオブジェクト (new Map())
キーの型string または symbol のみ制限なし(オブジェクト、関数、数値も可)
要素の順序基本的に保証されない(仕様上複雑)挿入順が完全に保証される
サイズ取得Object.keys(obj).length が必要map.size プロパティで一瞬
反復処理for...inObject.entries が必要Map 自体がイテラブル(直接 for...of 可)
パフォーマンス小規模な静的データに最適頻繁な追加・削除が発生する動的データに強い

いつMapを使うべきか?

以下の3つが主にMapを使うタイミングになります。

Mapを使うべきタイミング
  • キーとして「文字列以外」(例:ReactコンポーネントやDOM要素、他のオブジェクト)を使いたいとき
  • データの追加・削除が非常に頻繁に発生し、パフォーマンスがボトルネックになるとき
  • 格納したデータの「順番」を保持したままループ処理を行いたいとき

Map の型定義:キーと値に型を付ける方法

TypeScriptにおける Map の最大の武器は、キー(Key)と値(Value)の両方に厳格な型を付与できる点にあります。

基本の型定義(ジェネリクス)

new Map<K, V>() という形式で型を指定します。

// キーは数値(ID)、値はユーザー名の文字列
const userMap = new Map<number, string>();

userMap.set(1, "田中");
// userMap.set("2", "佐藤"); // Error: キーは number 型である必要があります

Union型やインターフェースの活用

実務では、特定の文字列のみをキーとして許可したり、複雑なオブジェクトを値に持たせたりすることが一般的です。

type Status = 'pending' | 'shipped' | 'delivered';
interface Order {
  id: string;
  amount: number;
}

const orderRegister = new Map<Status, Order[]>();

orderRegister.set('pending', [{ id: 'A1', amount: 5000 }]);

初期値からの型推論

初期値を渡す場合、TypeScriptは型を自動的に推論してくれます。

ただし、空のMapから始める場合は、型が Map<any, any> になってしまうのを防ぐため、必ずジェネリクスを明示しましょう。

Mapのメソッド(get, set, has, delete)とイテレーション

Map オブジェクトの操作は、オブジェクトリテラル(obj.key = value)とは異なり、専用のメソッドを介して行います。

ここにはTypeScript特有の「注意点」も存在します。

getメソッドの「undefined」の罠

最も注意すべきなのが get メソッドです。

Mapに指定したキーが存在しない場合、undefined が返されます。

const scores = new Map<string, number>();
scores.set("Math", 95);

const result = scores.get("Science"); 
// result の型は 'number | undefined' となるため、チェックなしで計算に使うとエラーになる
if (result !== undefined) {
  console.log(result.toFixed(1)); 
}

set, has, delete, clear

以下は、各メソッドの概要です。

各メソッドの概要
  • set(key, value): 要素を追加。チェーン(.set().set())が可能。
  • has(key): キーが存在するかチェック。戻り値は boolean
  • delete(key): 要素を削除。成功すれば true
  • clear(): すべての要素を削除。

イテレーション(ループ処理)

Map はそれ自体が反復可能(Iterable)なため、非常にシンプルにループを回せます。

const myMap = new Map<string, string>([["key1", "val1"], ["key2", "val2"]]);

// [key, value] のペアでループ (for...of)
for (const [key, value] of myMap) {
  console.log(`${key}: ${value}`);
}

// キーだけ、値だけを取り出す
const keys = Array.from(myMap.keys()); // ["key1", "key2"]
const values = Array.from(myMap.values()); // ["val1", "val2"]

【高度な型編】Mapped Typesで型を変換する

「TypeScript map」というキーワードにおいてMapped Types(マップ型)があります。

これは、実行時の Array.map が「配列の要素」を変換するように、コンパイル時に 「型のプロパティ」をループさせて新しい型を生成する機能です。

Mapped Typesの基本構文 {[P in K]: T}

Mapped Typesの基本構造は非常にシンプルですが、初見では少し特殊に見えるかもしれません。

基本構文の解剖

type NewType = {
  [P in K]: T;
};

K: 反復処理の対象となる「キーの集合」です。通常は string の Union型(例:'id' | 'name' | 'email')を指定します。

P: 現在処理されているプロパティ名を表す一時的な変数です。

in: 演算子。K に含まれるすべての型をループ処理することを意味します。

T: 各プロパティに割り当てる「値」の型です。

具体例:ユーザー設定のフラグ管理

例えば、複数の機能の有効/無効を管理する型を動的に作りたい場合を考えます。

type Feature = 'darkMode' | 'betaAccess' | 'notifications';

// Featureに含まれる各文字列をキーにし、値をbooleanにする
type FeatureConfig = {
  [P in Feature]: boolean;
};

/* 生成される型:
  type FeatureConfig = {
    darkMode: boolean;
    betaAccess: boolean;
    notifications: boolean;
  }
*/

元となるUnion型を一箇所変更するだけで、それを利用するすべての型定義が自動的に更新されるため、修正漏れが物理的に発生しなくなります。

Record型とMapped Typesの関係

Record<K, T> は、このMapped Typesを利用して定義された「組み込みのショートカット」に過ぎません。

Record型の内部実装

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

つまり、Record<string, number> と書くのは、内部的に {[P in string]: number} というマップ処理を走らせているのと同義です。

なぜ使い分けるのか?

Record型はシンプルなマッピング(すべてのキーに一律の型を割り当てる)に適しています。

Mapped Typesはキーごとに型を変えたり、既存のプロパティの属性(readonlyなど)を操作したりする「高度なカスタマイズ」が必要な時に使用します。

readonly や ? 修飾子の操作(Mapping Modifiers)

Mapped Typesの真の強みは、型を生成する際に 「修飾子(Modifiers)」を追加したり削除したりできる 点にあります。

これには +- という記号を接頭辞として使用します。

すべてのプロパティを「必須」にする

既存の型に ?(オプショナル)がついている場合、-? と記述することでそれらを強制的に必須にできます。

interface User {
  id: string;
  name?: string; // オプショナル
}

type RequiredUser = {
  [P in keyof User]-?: User[P];
};
// name が string (必須) に変換される

すべてのプロパティを「読み取り専用」にする

逆に、readonly 修飾子を動的に付与することも可能です。

type ReadonlyUser = {
  readonly [P in keyof User]: User[P];
};

既存の型をベースに「編集用(Partial)」「閲覧用(Readonly)」「バリデーション用(Required)」といった異なる役割の型を、元の定義を汚すことなく自由に派生させることができます。

テンプレートリテラル型との組み合わせによる高度なマッピング

モダンなTypeScript開発において、最も洗練されたテクニックが テンプレートリテラル型(Template Literal Types) との連携です。

as キーワード(Key Remapping)を使用することで、プロパティ名自体を文字列操作で作り変えることができます。

イベントハンドラの型を自動生成する

例えば、statussettings というキーワードから、onStatusChangeonSettingsChange という名前の関数プロパティを自動で生成する例を見てみましょう。

type PropName = 'status' | 'settings';

type ChangeHandlers = {
  [P in PropName as `on${Capitalize<P>}Change`]: (value: string) => void;
};

/*
  生成される型:
  type ChangeHandlers = {
    onStatusChange: (value: string) => void;
    onSettingsChange: (value: string) => void;
  }
*/

as 句の中でバッククォートを使うことで、プロパティ名を動的に組み立てています。

さらに Capitalize<T> というユーティリティ型を噛ませることで、先頭を大文字にする(キャメルケース/パスカルケースの調整)といった実務レベルの命名規則にも対応できます。

パフォーマンスとベストプラクティス

TypeScriptにおいて「Map」は「使いどころ」を誤ると、アプリケーションの動作が重くなったり、メモリを大量に消費したりする原因になります。

配列mapの計算量とメモ化の重要性

Array.prototype.map() は非常に便利な宣言的メソッドです。

ReactやNext.jsなどのフロントエンドフレームワークを使用している場合、コンポーネントが再レンダリングされるたびに map 処理が走り直すことに注意が必要です。

メモ化(Memoization)による最適化

これを防ぐための標準的なアプローチが useMemo を使ったメモ化です。

const expensiveList = useMemo(() => {
  return largeData.map(item => ({
    ...item,
    formattedDate: format(new Date(item.timestamp), 'yyyy/MM/dd')
  }));
}, [largeData]); // largeDataが変更された時だけ再計算

Mapオブジェクトのメモリ管理とWeakMap

Map オブジェクトは便利ですが、「強い参照(Strong Reference)」を保持し続けるという特性があります。

これが原因で、使い終わったはずのデータがメモリに残り続ける「メモリリーク」を引き起こすことがあります。

Mapオブジェクトの参照リスク

通常の Map にオブジェクトをキーとして保存すると、その Map 自体が破棄されない限り、キーとなったオブジェクトはガベージコレクション(GC)の対象になりません。

この問題を解決するのが WeakMap です。

WeakMap は、キーに対する参照を「弱く」持ちます。

スクロールできます
特徴MapWeakMap
キーの型制限なし(プリミティブも可)オブジェクトのみ
参照の強さ強い(GCを防ぐ)弱い(GCを妨げない)
反復処理可能(size, keys()等)不可能(中身を列挙できない)
主な用途汎用的な辞書、データのキャッシュメタデータの付与、プライベートデータ

実務での活用例:DOM要素へのメタデータ付与

例えば、特定のDOM要素に関連付けたデータを管理したい場合、WeakMap を使うのがベストプラクティスです。

const elementMetadata = new WeakMap<HTMLElement, { clickedCount: number }>();

function trackClick(el: HTMLElement) {
  const data = elementMetadata.get(el) || { clickedCount: 0 };
  data.clickedCount++;
  elementMetadata.set(el, data);
}

// el がDOMから削除され、どこからも参照されなくなれば、
// WeakMap内のデータも自動的にメモリから解放される。

通常の Map で行ってしまうと、DOM要素が削除されても Map 内に参照が残り続け、アプリを長く使えば使うほどメモリ使用量が増大していくことになります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次