TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - ジェネリクス配列操作

TypeScript TypeScript
スポンサーリンク

ゴール:「配列操作は全部 any で書けるけど、あえてジェネリクスで“型を守る”感覚をつかむ

配列操作は、正直 any[] でも全部書けます。
でもそれをやると、バグも型の穴も“侵入し放題”になります。

ジェネリクス配列操作の本質は、

「どんな要素型の配列でも扱える汎用性」と
「要素型をきっちり守る型安全性」

この両方を同時に満たすことです。

ここを押さえると、mapfilterfind みたいな処理を
自分で“安全に”設計できるようになります。

基本形:配列の要素型を 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 はない
TypeScript

any で書くと、こうはなりません。

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 のジェネリクス版

someevery は、戻り値が 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[], 自作の型の配列など、
いろいろな配列で呼び出してみてください。

そのとき、

「どんな配列でも扱えているのに、型はちゃんと守られている」

と感じられたら、
ジェネリクス配列操作の感覚はもうしっかり掴めています。

タイトルとURLをコピーしました