TypeScript:配列操作と型
1学習の目的
- 配列メソッドを使ったときに型がどう変化するかを理解する。JS09で学んだ高階関数が、TypeScriptでは型まで追跡されることを体験する。
- reduce で型を明示する方法と、filter で型を絞り込む方法を身につける。型推論が効かない場面での対処ができるようになる。
2基礎解説
| メソッド | 型の変化 | 注意点 |
|---|---|---|
| map | T[] → U[] | 戻り値の型で決まる |
| filter | T[] → T[] | 型は変わらない |
| reduce | T[] → 任意 | 初期値で型が決まる |
| find | T[] → T | undefined | 必ず undefined を考慮 |
- map はコールバックの戻り値で型が決まる。items.map(i => i.name) なら string[] になる。型注釈を書かなくても正しく追跡される。
- filter は型を変えない。件数は減るが要素の型はそのまま。null を除いても (string | null)[] のままなので、型ガードが必要になる。
- reduce は初期値から型が決まる。オブジェクトを組み立てるときは reduce<Record<string, number>> のように型引数を明示する。
- find の戻り値は必ず T | undefined。見つからない可能性があるので、そのままプロパティにアクセスするとエラーになる(TS08の ?. が活きる)。
- sort は元の配列を書き換える(JS06と同じ)。元を残したいなら [...items].sort(…) とコピーしてから並べ替える。
現場使用例:一覧の絞り込み、集計、ランキング作成、データの整形。実務のデータ処理はほぼすべてこの4つのメソッドの組み合わせで書ける。
type Product = { name: string; price: number; category: string };
const items: Product[] = [
{ name: "ノートPC", price: 128000, category: "PC" },
{ name: "マウス", price: 3200, category: "周辺機器" },
];
// map:Product[] → string[] に変わる
const names = items.map((i) => i.name); // string[]
// filter:型は Product[] のまま
const cheap = items.filter((i) => i.price < 10000); // Product[]
// reduce:初期値 0 なので number
const total = items.reduce((sum, i) => sum + i.price, 0);
// reduce でオブジェクトを作る(型引数を明示する)
const byCategory = items.reduce<Record<string, number>>(
(acc, i) => {
acc[i.category] = (acc[i.category] ?? 0) + 1;
return acc;
},
{}
);
// find は undefined になりうる
const found = items.find((i) => i.name === "存在しない");
console.log(found?.name ?? "未発見");
// ⚠ filter だけでは null が消えない(型の上では)
const mixed: (string | null)[] = ["a", null, "b"];
const bad = mixed.filter((v) => v !== null); // (string | null)[] のまま
// ✅ 型ガードを書けば string[] になる
const good = mixed.filter((v): v is string => v !== null); // string[]
3基本ドリル(10問)
出力 == ノートPC, マウス
map で name を取り出す。結果は string[] になるので join できる。
type Product = { name: string; price: number };
const items: Product[] = [
{ name: "ノートPC", price: 128000 },
{ name: "マウス", price: 3200 },
];
console.log(items.map((i) => i.name).join(", "));
map で型が Product[] から string[] に変わる。だから join が使える——もし数値を返していたら number[] になり、join の挙動も変わる。
A. 変わらず Product[] B. 別の型になる C. any[] D. unknown[] 選択★☆☆無料
解答 == A
filter は要素を減らすだけで、変換はしない。
A
filter は件数を減らすが型は変えない。だから null を除外しても、型の上では null が残ったままになる——ここが落とし穴。
出力 == 2
filter で条件に合うものだけを残し、length で数える。
type Product = { name: string; price: number };
const items: Product[] = [
{ name: "ノートPC", price: 128000 },
{ name: "マウス", price: 3200 },
{ name: "キーボード", price: 8500 },
];
console.log(items.filter((i) => i.price < 10000).length);
filter の中の i は Product と推論される。i.pric のような打ち間違いは、その場でエラーになる。
const prices = [128000, 3200, 8500]; const total = prices.____((sum, p) => sum + p, 0); console.log(total);
出力 == 139700
配列を1つの値にまとめるメソッド。小文字6文字。
reduce
初期値の 0 が number なので、結果も number と推論される。初期値を "" にすれば文字列の連結になる。
A. T B. T | undefined C. T[] D. T | null 選択★☆☆無料
解答 == B
見つからない場合がある。
B
find は見つからないと undefined を返す。そのまま .name と書くとエラーになるので、?. や条件分岐が必要(TS08)。
出力 == PC:1, 周辺機器:2
reduce
type Product = { name: string; category: string };
const items: Product[] = [
{ name: "ノートPC", category: "PC" },
{ name: "マウス", category: "周辺機器" },
{ name: "キーボード", category: "周辺機器" },
];
const counts = items.reduce<Record<string, number>>((acc, i) => {
acc[i.category] = (acc[i.category] ?? 0) + 1;
return acc;
}, {});
console.log(
Object.entries(counts)
.map(([k, v]) => `${k}:${v}`)
.join(", ")
);
型引数を書かないと、初期値の {} から「空オブジェクト」と推論されてエラーになる。reduce でオブジェクトを組み立てるときは型引数の明示が定石。
const mixed: (string | null)[] = ["a", null, "b"];
const strs = mixed.filter((v): v ____ string => v !== null);
console.log(strs.join(","));
出力 == a,b
型ガード関数の書き方(TS09)。2文字のキーワード。
is
これを書かないと (string | null)[] のまま。null を除いたつもりでも型が変わらないので、join の前にエラーになることがある。
A. 何も起きない B. 元の配列が並べ替えられる C. 新しい配列が返るだけ D. エラーになる 選択★★☆無料
解答 == B
sort は破壊的メソッド(JS06)。
B
元の配列が書き換えられる。元の順序を保ちたいなら [...items].sort(…) とコピーしてから並べ替える。
出力 == ノートPC
[...items].sort((a, b) => b.price - a.price) とコピーしてから並べ替える。
type Product = { name: string; price: number };
const items: Product[] = [
{ name: "ノートPC", price: 128000 },
{ name: "マウス", price: 3200 },
{ name: "モニター", price: 45000 },
];
const sorted = [...items].sort((a, b) => b.price - a.price);
console.log(sorted[0].name);
スプレッド構文でコピーを作るのが安全な作法。元の配列を並べ替えてしまうと、他の場所での処理に影響が出る。
出力2行。true / true
some と every を使う(JS09)。どちらも boolean を返す。
type Product = { name: string; price: number };
const items: Product[] = [
{ name: "ノートPC", price: 128000 },
{ name: "マウス", price: 3200 },
];
console.log(items.some((i) => i.price > 100000));
console.log(items.every((i) => i.price >= 1000));
some は「1つでもあれば true」、every は「全部満たせば true」。条件チェックを簡潔に書ける。
4実践シナリオ(5問)
出力3行が完全一致
map で売上を含む配列を作り、sort で並べ替え、forEach で番号付きで出力する。
type Item = { name: string; price: number; qty: number };
const items: Item[] = [
{ name: "ノートPC", price: 128000, qty: 3 },
{ name: "マウス", price: 3200, qty: 15 },
{ name: "モニター", price: 45000, qty: 1 },
];
const sales = items
.map((i) => ({ name: i.name, total: i.price * i.qty }))
.sort((a, b) => b.total - a.total);
sales.forEach((s, index) => {
console.log(`${index + 1}位 ${s.name}:${s.total.toLocaleString()}円`);
});
map で新しい形のオブジェクトに変換している。{ name: string; total: number }[] という型が自動で推論されるので、sort の中でも a.total が使える。
出力 == PC:173,000円 / 周辺機器:11,700円
reduce
type Product = { name: string; category: string; price: number };
const items: Product[] = [
{ name: "ノートPC", category: "PC", price: 128000 },
{ name: "マウス", category: "周辺機器", price: 3200 },
{ name: "モニター", category: "PC", price: 45000 },
{ name: "キーボード", category: "周辺機器", price: 8500 },
];
const totals = items.reduce<Record<string, number>>((acc, i) => {
acc[i.category] = (acc[i.category] ?? 0) + i.price;
return acc;
}, {});
console.log(
Object.entries(totals)
.map(([k, v]) => `${k}:${v.toLocaleString()}円`)
.join(" / ")
);
SQL07のGROUP BYと同じことをしている。?? 0 で初回の未定義に対処するのは、SQL05のCOALESCEと同じ発想。
出力 == 600
filter((v): v is number => v !== null) と書く。これで number[] になり reduce できる。
const values: (number | null)[] = [100, null, 200, null, 300]; const numbers = values.filter((v): v is number => v !== null); const total = numbers.reduce((sum, n) => sum + n, 0); console.log(total);
型ガードを書かないと (number | null)[] のままで、reduce の中で sum + n がエラーになる。null が混ざる可能性を型が覚えているため。
「全4件/在庫あり3件」
「要発注:ノートPC(5), モニター(7)」(在庫10未満)
「在庫総数:54個」
データ:ノートPC/5、マウス/42、モニター/7、サンプル/0 コーディング★★★無料
出力3行が完全一致
filter で在庫ありと要発注を分ける。合計は reduce。
type Item = { name: string; stock: number };
const items: Item[] = [
{ name: "ノートPC", stock: 5 },
{ name: "マウス", stock: 42 },
{ name: "モニター", stock: 7 },
{ name: "サンプル", stock: 0 },
];
const inStock = items.filter((i) => i.stock > 0);
const lowStock = items.filter((i) => i.stock > 0 && i.stock < 10);
const total = items.reduce((sum, i) => sum + i.stock, 0);
console.log(`全${items.length}件/在庫あり${inStock.length}件`);
console.log(
`要発注:${lowStock.map((i) => `${i.name}(${i.stock})`).join(", ")}`
);
console.log(`在庫総数:${total}個`);
同じ配列に対して複数の集計を行う典型パターン。filter は元の配列を変えないので、何度でも別の条件で絞り込める。
出力2行。マウス:3,200円 / 該当なし
find の戻り値は Product | undefined。?. と ?? を組み合わせる(TS08)。
type Product = { name: string; price: number };
const items: Product[] = [
{ name: "ノートPC", price: 128000 },
{ name: "マウス", price: 3200 },
];
function findByName(name: string): Product | undefined {
return items.find((i) => i.name === name);
}
for (const name of ["マウス", "存在しない商品"]) {
const found = findByName(name);
console.log(
found ? `${found.name}:${found.price.toLocaleString()}円` : "該当なし"
);
}
find の戻り値を素直に | undefined で表現している。呼び出し側は必ず存在チェックを迫られるので、確認漏れが起きない。
5仕上げ課題
データ:注文配列
type Order = { product: string; category: string; price: number; qty: number };
・ノートPC / PC / 128000 / 2
・マウス / 周辺機器 / 3200 / 10
・モニター / PC / 45000 / 1
・キーボード / 周辺機器 / 8500 / 3
処理
① 各注文の売上(price × qty)を計算
② 売上の多い順に並べ替え(元の配列は変更しない)
③ カテゴリ別の売上を集計(reduce)
④ 5万円以上の注文だけを抽出
期待される出力(8行)
=== 売上ランキング ===
1位 ノートPC:256,000円
2位 モニター:45,000円
3位 マウス:32,000円
4位 キーボード:25,500円
--- カテゴリ別 ---
PC:301,000円 / 周辺機器:57,500円
5万円以上の注文:1件
出力8行が完全一致
map で売上を含む配列を作り、[...].sort() で並べ替える。カテゴリ別は reduce
type Order = {
product: string;
category: string;
price: number;
qty: number;
};
const orders: Order[] = [
{ product: "ノートPC", category: "PC", price: 128000, qty: 2 },
{ product: "マウス", category: "周辺機器", price: 3200, qty: 10 },
{ product: "モニター", category: "PC", price: 45000, qty: 1 },
{ product: "キーボード", category: "周辺機器", price: 8500, qty: 3 },
];
const sales = orders.map((o) => ({
product: o.product,
category: o.category,
amount: o.price * o.qty,
}));
console.log("=== 売上ランキング ===");
const ranked = [...sales].sort((a, b) => b.amount - a.amount);
ranked.forEach((s, index) => {
console.log(`${index + 1}位 ${s.product}:${s.amount.toLocaleString()}円`);
});
console.log("--- カテゴリ別 ---");
const byCategory = sales.reduce<Record<string, number>>((acc, s) => {
acc[s.category] = (acc[s.category] ?? 0) + s.amount;
return acc;
}, {});
console.log(
Object.entries(byCategory)
.map(([k, v]) => `${k}:${v.toLocaleString()}円`)
.join(" / ")
);
const large = sales.filter((s) => s.amount >= 50000);
console.log(`5万円以上の注文:${large.length}件`);
4つの配列メソッドが、それぞれの役割で連携している。map で形を変え、sort で順序を決め、reduce でまとめ、filter で絞る——JS09で学んだ操作が、型に守られたまま使えている。
とくに map の結果に注目してほしい。{ product: string; category: string; amount: number }[] という新しい型が自動的に生まれている。以降の sort や reduce の中で s.amount と書けるのは、この推論のおかげだ。s.amout と打ち間違えれば即座に赤線が出る。
[...sales].sort(…) のコピーも重要だ。もし sales.sort(…) と書いていたら、その後の reduce や filter は並べ替え済みの配列を処理することになる。今回は結果が変わらないが、「元データを壊さない」という原則を守るほうが、後から機能を足すときに安全になる。
次章ではクラスを学ぶ。JS14で学んだクラスに型が加わると、より厳密な設計ができるようになる。