ゴール:「配列操作は全部 any で書けるけど、あえてジェネリクスで“型を守る”感覚をつかむ
配列操作は、正直 any[] でも全部書けます。
でもそれをやると、バグも型の穴も“侵入し放題”になります。
ジェネリクス配列操作の本質は、
「どんな要素型の配列でも扱える汎用性」と
「要素型をきっちり守る型安全性」
この両方を同時に満たすことです。
ここを押さえると、map・filter・find みたいな処理を
自分で“安全に”設計できるようになります。
基本形:配列の要素型を T で表す
ジェネリクス配列の一番シンプルな例
まずは「配列の先頭要素を返す」関数から。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b", "c"]); // s: string | undefined
const u = first([{ id: 1 }]); // u: { id: number } | undefined
TypeScriptここでやっていることはシンプルです。
Tは「配列の要素型」T[]は「要素型が T の配列」- 戻り値は「T かもしれないし、要素がなければ undefined」
重要なのは、any[] を一切使っていないのに、どんな配列でも扱えていることです。
any[] で書くとこうなります。
function firstAny(arr: any[]): any {
return arr[0];
}
const x = firstAny([1, 2, 3]); // x: any
x.toFixed(2); // コンパイルは通るが、本当に number かは分からない
TypeScriptジェネリクス版は、戻り値の型が「渡した配列の要素型」にきっちり対応します。
これは、「型の整合性が崩れない」という意味で、セキュリティ的にも堅い設計です。
map をジェネリクスで書く:入力型と出力型を分けて考える
TInput と TOutput を使った map 関数
配列操作の代表格 map を、自分でジェネリクスで書いてみます。
function mapArray<TInput, TOutput>(
arr: TInput[],
fn: (value: TInput, index: number, array: TInput[]) => TOutput
): TOutput[] {
return arr.map(fn);
}
const lengths = mapArray(["a", "bb", "ccc"], (s) => s.length);
// lengths: number[]
const upper = mapArray(["a", "bb"], (s) => s.toUpperCase());
// upper: string[]
TypeScriptここでのポイントは 2 つです。
1つ目は、「元の配列の要素型」と「変換後の要素型」を分けていること。TInput が入力要素型、TOutput が出力要素型です。
2つ目は、コールバック関数 fn の型にも TInput/TOutput をきっちり反映していること。
これにより、fn の中で「ありえない操作」をするとコンパイルエラーになります。
mapArray(["a", "bb"], (s) => s.toFixed(2)); // エラー:string に toFixed はない
TypeScriptany で書くと、こうはなりません。
function mapAny(arr: any[], fn: (value: any) => any): any[] {
return arr.map(fn);
}
const result = mapAny(["a", "bb"], (s) => s.toFixed(2)); // コンパイルは通る
TypeScriptこの差は、「型の破綻をコンパイル時に検知できるかどうか」という意味で、
セキュリティの“入力検証”に近い感覚です。
filter をジェネリクスで書く:要素型を保ったまま絞り込む
単純な filter のジェネリクス版
次は filter を考えます。
function filterArray<T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): T[] {
return arr.filter(predicate);
}
const nums = filterArray([1, 2, 3, 4], (n) => n % 2 === 0);
// nums: number[]
const longWords = filterArray(["a", "bb", "ccc"], (s) => s.length >= 2);
// longWords: string[]
TypeScriptここでは、入力配列と出力配列の要素型は同じ T です。
「絞り込むだけで、型は変わらない」からです。
any[] で書くと、戻り値は常に any[] になり、
「この配列の中身は何か?」が型から分からなくなります。
ジェネリクスで書くことで、
- 「元が number[] なら、結果も number[]」
- 「元が User[] なら、結果も User[]」
という関係が、型レベルで保証されます。
型ガードと組み合わせた filter
もう一歩進めると、型ガードと組み合わせることもできます。
type User = { id: number; name: string };
type Guest = { guestId: string };
function isUser(value: User | Guest): value is User {
return "id" in value;
}
function filterUsers<T>(
arr: T[],
predicate: (value: T) => value is User
): User[] {
return arr.filter(predicate);
}
const mixed: (User | Guest)[] = [
{ id: 1, name: "Taro" },
{ guestId: "g-1" },
];
const users = filterUsers(mixed, isUser);
// users: User[]
TypeScriptここでは、「フィルタの結果、要素型が User に“絞り込まれる”」ことを
ジェネリクス+型ガードで表現しています。
これは、セキュリティ的に言えば、
「入力が混在していても、検査を通ったものだけを“安全な型”として扱う」
という“ホワイトリスト的な設計”に近いです。
find / some / every なども同じ発想で書ける
find のジェネリクス版
find も同じノリで書けます。
function findInArray<T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): T | undefined {
return arr.find(predicate);
}
const found = findInArray([1, 2, 3, 4], (n) => n > 2);
// found: number | undefined
TypeScript戻り値が T | undefined になっているのがポイントです。
「見つからない可能性」を型で表現しています。
これを any で書くと、戻り値が any になり、
「undefined かもしれない」という情報が消えます。
「存在しないかもしれないものを、存在する前提で扱ってしまう」
これはバグの温床であり、セキュリティ的にも危険なパターンです。
some / every のジェネリクス版
some や every は、戻り値が boolean なのでシンプルです。
function someInArray<T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): boolean {
return arr.some(predicate);
}
function everyInArray<T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): boolean {
return arr.every(predicate);
}
TypeScriptここでも、predicate の引数が T であることが重要です。
これにより、「T に存在しないプロパティを見ようとしたらコンパイルエラー」になります。
type User = { id: number; active: boolean };
const users: User[] = [
{ id: 1, active: true },
{ id: 2, active: false },
];
everyInArray(users, (u) => u.active); // OK
everyInArray(users, (u) => u.enabled); // エラー:enabled は存在しない
TypeScriptこれは、「存在しないフィールドでアクセス制御しようとしている」ような
危険なコードを、コンパイル時に止めてくれるイメージです。
セキュリティ視点で見る「ジェネリクス配列操作」の価値
any[] は「検査なしの入力」を許すのと同じ
any[] を使った配列操作は、
セキュリティでいうと「入力検証を一切しない API」に近いです。
- 何が入ってくるか分からない
- 何を返しているかも分からない
- 間違った使い方をしてもコンパイルは通る
これは、「型の境界がガバガバ」な状態です。
ジェネリクス配列操作は、
- 受け取る配列の要素型を T として明示
- その T に基づいて、コールバックや戻り値の型をきっちり決める
ことで、「型の境界を強固にする」役割を果たします。
「どんな型でも扱える」のに「型は崩れない」
ジェネリクス配列操作の一番の美点は、
- number[] でも string[] でも User[] でも扱える汎用性
- それでいて、要素型が常に正しく保たれる型安全性
この両立です。
セキュリティの世界でよく言う、
「柔軟だけど、境界は厳格」
という設計にかなり近いです。
まとめ:ジェネリクス配列操作を自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリクス配列操作は、
「配列の要素型を T という“型の変数”で表し、
どんな配列でも扱えるようにしつつ、
T に基づいて型安全を守る書き方」。
mapArray<TInput, TOutput> のように、
入力と出力の要素型を分けて設計することで、
- 変換前後の型の関係が崩れない
- 間違った操作はコンパイル時に止まる
という“堅い API”になる。
まずは、自分の手で次の 3 つを書いてみてください。
function first<T>(arr: T[]): T | undefined { return arr[0]; }
function mapArray<TInput, TOutput>(arr: TInput[], fn: (v: TInput) => TOutput): TOutput[] { return arr.map(fn); }
function filterArray<T>(arr: T[], predicate: (v: T) => boolean): T[] { return arr.filter(predicate); }
TypeScriptそして、number[], string[], 自作の型の配列など、
いろいろな配列で呼び出してみてください。
そのとき、
「どんな配列でも扱えているのに、型はちゃんと守られている」
と感じられたら、
ジェネリクス配列操作の感覚はもうしっかり掴めています。

