1. ホーム
  2. 記事一覧
  3. 初心者向け!React × TypeScriptでジェネリクスを理解しよう

2025.01.08

初心者向け!React × TypeScriptでジェネリクスを理解しよう

はじめに

ジェネリクスは、TypeScriptにおいて汎用性の高い型を安全に扱うための重要な仕組みです。しかし、TypeScriptを学び始めたフロントエンド初心者には難しく感じるかもしれません。

この記事では、ジェネリクスの基本をやさしく解説し、ReactとTypeScriptを組み合わせた実践例を通じて、具体的な使い方を解説しています。記事を通して、ジェネリクスを活用して型定義ができるようになることを目指します。初めて学ぶ方にも理解しやすいよう、順を追って説明していますので、一緒に学んでいきましょう。

記事の流れは以下の通りとなっています。ジェネリクスを段階的に理解できるように、シンプルな例から解説し、段階的に実践のコード記述を紹介していきます。

  • ジェネリクスの基本構文の解説
  • Reactコンポーネントでのシンプルなジェネリクス使用例
  • ジェネリクスを使ったTodoアプリハンズオン

記事の目的と対象読者

目的

  • TypeScript のジェネリクスの基本と活用方法を理解
  • React × TypeScriptでジェネリクスを利用した型定義ができるようになること

対象者

  • フロントエンドを学習している初心者
  • ジェネリクスの型定義に苦手意識のある方

前提知識と関連記事の紹介

TypeScriptの基本的な型定義、Reactの基礎についてはこの記事では解説していません。以下の記事で詳しく解説していますので、ぜひあわせてご覧ください。

1.TypeScriptのジェネリクスとは何か

ジェネリクスとは、コードの共通化と型の安全性を両立するためのTypeScriptの機能です。型を関数の「引数」のように扱い、柔軟で型安全なコードを実現できます。

1.1 基本的なジェネリクス関数の書き方

ジェネリクスを使用した基本構文は以下となります。

function sampleFunction<T>(arg: T): T {
  return arg;
}

T は型の「型パラメーター」であり、実際には呼び出し時に型が推論されます。型パラメーターとは、ジェネリクスで使用される「型を動的に指定するための変数」です。通常の関数が値を引数として受け取るように、ジェネリクスでは型パラメーターを通じて型を受け取ります

例えば以下の引数の値を返す関数では、Tが型パラメーターとして定義されています。

function generics<T>(value: T): T {
  return value;
}

const num = generics<number>(42); // T は number
const str = generics<string>("hello"); // T は string

Tに適用される型は、関数identityを呼び出す際に指定されます。

この説明だけではわかりづらいと思いますので、コード例とともにもう詳しく説明していきます。はじめに、ジェネリクスを「使用しない型定義」と「使用した型定義」の違いを知りましょう。

1.2 ジェネリクスを使用しない型定義

以下のように、あらかじめ型を指定した関数を定義したとします。

function returnValue(value: number): number {
  return value;
}

この場合、関数は数値のみに対応し、他の型の値を渡すとエラーになります。

const num = returnValue(42); // 正常: 引数も戻り値もnumber型
console.log(num); // 42

const str = returnValue("Hello"); // エラー: string型は渡せません
console.log(str);

異なる型に対応するためには、型ごとに別々の関数を用意する必要がありますが、それではコードが冗長になり見通しが悪くなります。以下のように、同じような処理をする関数が増えてしまいます。

// number型用の関数
function numValue(value: number): number {
  return value;
}

// string型用の関数
function strValue(value: string): string {
  return value;
}

// 数値を渡す
const num = numValue(42); // 正常
console.log(num); // 42

// 文字列を渡す
const str = strValue("Hello"); // 正常
console.log(str); // "Hello"

1.3 ジェネリクスを使用した型定義

このような場面でジェネリクスを使うと、1つの関数で異なる型に対応できるようになります。

// ジェネリクスで型定義した関数
function returnValue<T>(value: T): T {
  return value;
}

// 数値を渡す場合
const num = returnValue<number>(42); // <T>にnumber型を指定
console.log(num); // 42

// 文字列を渡す場合
const str = returnValue<string>("Hello"); // <T>にstring型を指定
console.log(str); // "Hello"

この関数のジェネリクスTは、型の「変数」として機能します。この関数を呼び出す際に、型を指定することができます。

1.4 関数の呼び出し側から渡される型

数値(number 型)を渡す場合、Tnumber に置き換えられます。

// ジェネリクスで型定義した関数
function returnValue<T>(value: T): T {
  return value;
}

↑↑↑↑ number 型が関数のT部分に渡される

const num = returnValue<number>(42); // number型をTに渡す
console.log(num); // 42

文字列(string 型)を渡す場合、Tstring に置き換えられます。

// ジェネリクスで型定義した関数
function returnValue<T>(value: T): T {
  return value;
}

↑↑↑↑ string 型が関数のT部分に渡される

const str = returnValue<string>("Hello"); // string型をTに渡す
console.log(str); // "Hello"

このようにジェネリクスを使うことで、コードの柔軟性と再利用性が大幅に向上します。

1.5 TypeScriptの型推論について補足

TypeScript では、引数から型を自動的に推論することも可能のため、以下の例のように型の記述を省略することができます。このセクションではジェネリクスの仕組みを理解するために、型を明示的に指定しています。

const num = returnValue(42); // T を number と推論
const str = returnValue("Hello"); // T を string と推論

2.ジェネリクスをさらに理解する

ジェネリクスの基本を押さえたら、次のステップとしてその応用や仕組みを学びましょう。このセクションでは、型パラメータの種類や制約(extends)、初期値の指定、複数の型パラメータを使った定義方法について解説します。

2.1 型パラメーターの種類は複数ある

前のセクションではTの型パラメーターを説明しましたが、パラメーターには他に以下の複数の種類が使用されています。以下は、ジェネリクスで使用される主な型パラメーター例と、その用途をまとめた表です。他にも状況に応じて自由に命名することができます。

型パラメーター意味主な用途
TType任意の型を表す場合に使用する汎用的な型
KKeyオブジェクトのキーを表す場合に使用
UUnknownジェネリクスの型が未知の場合に使用
EElement配列や HTML 要素の型に使用

型パラメーターは単なる記号ではなく、意味を持たせることでコードの可読性が向上します。実際には、プロジェクトや開発チームによって異なる命名規則が採用される場合もあります。適切な型パラメーターを活用して、コードの読みやすさと保守性を向上させましょう。

2.2 型パラメーターに制約を付けるextendsの使い方

型パラメーターに制約を付けることで、特定の型構造を持つことを保証できます。以下の例では、T extends { id: number }の部分で、ジェネリクス T に「idプロパティ(数値型)を持つオブジェクト」であることを制約しています。

// id プロパティ(数値型)を持つオブジェクト」を制約
function printId<T extends { id: number }>(arg: T): number {
  return arg.id;
}

// 使用例:1 正常
const id = printId({ id: 202, name: "Alice" });
console.log(id); // 202

// 使用例:2 エラー
const id = printId({ name: "Alice" }); 
// エラー: id プロパティが存在しない

ジェネリクスは柔軟にさまざまな型を扱えるため、場合によっては型安全性や保守性を保つことが難しくなります。渡す型があらかじめ分かってる場合は、extendsを使って型に制約を付けることで、安全で明確なコードを書くことができます。

2.3 型パラメーターにデフォルト値を指定

ジェネリクスでは、型パラメーターにデフォルト値を設定することができます。これにより、型を明示的に指定しなくても、デフォルト値が自動的に適用されます。

// デフォルト型として string を設定
function createArray<T = string>(length: number, value: T): T[] {
  return Array.from({ length }, () => value);
}

// 使用例
const defaultArray = createArray(3, "hello"); // T は string
const numberArray = createArray<number>(3, 42); // T は number

この例では、<T = string>という記述によって、型を指定しない場合はTが自動的にstring型として扱われます。そのため、createArray(3, "hello")のように呼び出した場合、Tが省略されても問題ありません。一方、型を明示的に指定したい場合は、<number>のように型を指定して使うこともできます。

2.4 複数の型パラメーターを指定

ジェネリクスでは、複数の型パラメーターを使用して、異なる型のデータを安全に組み合わせる関数を作成できます。次の例では、2つの値をペアとして返す関数を定義しています。

function createPair<K, T>(first: K, second: T): [K, T] {
    return [first, second];
}

// 使用例
const pair = createPair<number, string>(1, "Hello");
console.log(pair); // 出力: [1, "Hello"]
  • createPair<K, T>(first: K, second: T)

    Kは最初の値firstの型、Tは2つ目の値secondの型をあらわします。

  • : [K, T]

    関数の戻り値はタプル型で、1つ目の要素がK型、2つ目の要素がT型として定義されています。

このように複数の型パラメーターを使用することで、異なる型のデータを柔軟に取り扱えるだけでなく、型安全性も向上します。

3. React × TypeScript でのジェネリクス型定義

ここまでTypeScriptでのジェネリクス型定義について学びました。このセクションでは、ReactとTypeScriptを組み合わせてジェネリクスを活用する方法を解説します。Reactの基本要素であるPropsとuseStateを例に、ジェネリクス型定義の実践方法を学びましょう。

3.1 Propsにジェネリクスを使い再利用性を高める

ジェネリクスを活用すると、渡されるPropsの型を柔軟に定義できます。以下に、1つの値をPropsとして受け取るシンプルなコンポーネント例です。

type OptionalProps<T> = {
  value: T; // 任意の型の値を受け取る
};

const Optional = <T,>({ value }: OptionalProps<T>): JSX.Element => {
  return <div>{value}</div>;
};

// 使用例
const App = () => {
  return (
    <div>
      <Optional<number> value={42} /> {/* 数値を渡す */}
      <Optional<string> value="Hello, TypeScript!" /> {/* 文字列を渡す */}
    </div>
  );
};

export default App;

OptionalProps<T>Tは、valueの型を任意に指定できる型パラメーターです。数値や文字列など異なる型を柔軟に受け取ることができます。ジェネリクスを使うことで、同じコンポーネントで異なる型のデータを扱えるため、コードの再利用性が向上します。

注意点

この例は、単純な値を表示する用途には適していますが、型ごとに異なる処理が必要な場合には不向きです。ジェネリクスを多用すると、意図が曖昧になりやすいため、機能に応じて適切に採用しましょう。

3.2 useStateの型定義

Reactの状態管理に使われるuseStateでもジェネリクスを指定することで、状態の型を明確に管理できます。以下に、カウントアップ機能を持つコンポーネントの例を示します。

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState<number>(0); // 初期値は0

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
};

export default App;
  • ジェネリクスの指定

    useState<number>と記述することで、状態countの型をnumberに限定しています。これにより、型安全性が向上し、不正な型のデータが代入されるリスクを防ぎます。

  • 型推論

    初期値を渡す場合、TypeScriptが型を自動的に推論するため、ジェネリクスの記述を省略することも可能です(例: useState(0))。

4.ジェネリクスを使った実践ハンズオン

このセクションでは、Todoアプリを作成しながらジェネリクスの実践的な使い方を学びます。コードを段階的に実装し、ジェネリクスを用いた型定義の理解を深めましょう。

完成イメージ

以下は、ハンズオンで作成するTodoアプリの完成イメージです。このアプリには次の機能を実装します。

  • フォームから入力された値をTodoリストに追加
  • 追加したTodoを削除

ハンズオンにおすすめのツール

コードを簡単に試したい方には、以下のオンラインツールがおすすめです。フロントエンド環境を構築する必要がなく、ブラウザ上で手軽にコードを実行できます。

StackBlitzの使い方やメリットについては、以下の記事で詳しく解説しています。ハンズオンの準備に活用してください。

1. Todo型を定義

プロジェクトを作成した後、App.tsxにTodoを管理するための型を定義します。この型は**型エイリアス (Type Alias)**を使い、Todoアイテムの識別子(id)と内容(text)を表します。後にuseStateでこの型を利用して状態を管理します。

// App.psx

// Todoを管理する型
type Todo = {
  id: number; // Todo の一意な識別子
  text: string; // Todo の内容を表す文字列
};

プロジェクトの作成は以下の記事の「アプリケーションの開発準備」セクションで紹介していますので、ぜひご覧ください。

初心者向け!React × TypeScriptで作る天気予報アプリハンズオン

https://envader.plus/article/520

2. Todoリストの状態を管理

次に、Todoリストを管理するためのuseStateを記述します。ジェネリクスを使用してTodo[]型を指定します。 これにより、useStateが管理する状態(todos)が、Todo型のデータしか含まない配列であることを保証できます。

// App.psx

// Todoを管理する型
type Todo = {
  id: number; // Todo の一意な識別子
  text: string; // Todo の内容を表す文字列
};

const App = () => {

// 以下を追加する ↓↓↓

  const [todos, setTodos] = useState<Todo[]>([]); // Todoの配列を管理

  return (
    <div>
      <h1>Todo List</h1>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
};

// 追加ここまで↑↑↑

export default App;

3. 新しいTodoを追加するフォームを作成

新しいTodoを追加するフォームを作成します。入力値はuseState<string>で管理し、addTodo関数を用いてTodoリストに追加します。フォームに入力した内容がリストに反映されるよう実装します。

import { useState } from 'react';

// Todoを管理する型
type Todo = {
  id: number; // Todo の一意な識別子
  text: string; // Todo の内容を表す文字列
};

function App() {
  const [todos, setTodos] = useState<Todo[]>([]); // Todoの配列を管理

  // 以下を追加する ↓↓↓

  const [newTodo, setNewTodo] = useState<string>('');

  const addTodo = () => {
    if (newTodo.trim() === '') return; // 空のTodoは追加しない
    const newTodoItem: Todo = {
      id: Date.now(), // 新しいTodoを追加
      text: newTodo, // フォームをリセット
    };
    setTodos([...todos, newTodoItem]);
    setNewTodo('');
  };

  // 追加ここまで↑↑↑

  return (
    <>
      <h1>TodoLists</h1>

      {/* 以下を追加する ↓↓↓ */}

      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)} // 入力値を更新
        placeholder="新しいタスクを入力"
      />
      <button onClick={addTodo}>タスクを追加</button>

      {/* 追加ここまで↑↑↑ */}

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

export default App;

4. Todoの削除機能を追加

追加されたTodoを削除する機能を実装します。deleteTodo関数を使い、指定したTodoのidをもとにリストから削除します。Todoリストに削除ボタンを追加し、クリックすると対応するTodoリストが削除される仕組みです。

import { useState } from 'react';

type Todo = {
  id: number;
  text: string;
};

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState<string>('');

  const addTodo = () => {
    if (newTodo.trim() === '') return; // 空のTodoは追加しない
    const newTodoItem: Todo = {
      id: Date.now(), // 新しいTodoを追加
      text: newTodo, // フォームをリセット
    };
    setTodos([...todos, newTodoItem]);
    setNewTodo('');
  };
  
  // 以下を追加する ↓↓↓

  const deleteTodo = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  // 追加ここまで↑↑↑

  return (
    <>
      <h1>TodoLists</h1>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)} // 入力値を更新
        placeholder="新しいタスクを入力"
      />
      <button onClick={addTodo}>タスクを追加</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.text}

            {/* 以下を追加する ↓↓↓ */}
            
            <button onClick={() => deleteTodo(todo.id)}>タスクを削除</button>

            {/* 追加ここまで↑↑↑ */}

          </li>
        ))}
      </ul>
    </>
  );
}

export default App;

5. 完成コード

以下が全体の完成コードです。

import { useState } from 'react';

// Todoを管理する型
type Todo = {
  id: number; // Todo の一意な識別子
  text: string; // Todo の内容を表す文字列
};

function App() {
  const [todos, setTodos] = useState<Todo[]>([]); // Todoの配列を管理
  const [newTodo, setNewTodo] = useState<string>(''); // 入力フォームの値を管理

  const addTodo = () => {
    if (newTodo.trim() === '') return; // 空のTodoは追加しない
    const newTodoItem: Todo = {
      id: Date.now(), // 一意な識別子を生成
      text: newTodo, // 入力された文字列をタスク内容として使用
    };
    setTodos([...todos, newTodoItem]); // 新しいTodoを既存のリストに追加
    setNewTodo(''); // フォームをリセット
  };

  const deleteTodo = (id: number) => {
    // 指定されたidと一致しないTodoだけを残した新しい配列を作成し、現在のTodoリストを更新する
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <>
      <h1>TodoLists</h1>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)} // 入力値を更新
        placeholder="新しいタスクを入力"
      />
      <button onClick={addTodo}>タスクを追加</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.text}
            <button onClick={() => deleteTodo(todo.id)}>タスクを削除</button>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;

コード編集後、以下のようなTodoの追加と削除ができるアプリになっているかと思います。

※以下のイメージ図はCSSを適用しています。CSSのコード説明は省略させていただきます。

このハンズオンを通して、ジェネリクスの型定義について学びました。今後のステップとして、ご自身で以下のような機能を追加し、ジェネリクスの理解をさらに深めてみてはいかがでしょうか。

  • Todoリスト表示部分をコンポーネントとして分割し、Propsにジェネリクスを適用
  • Todoリストの更新機能の追加

ご自身が考えた追加機能を実装することは、プログラミングスキルの向上につながります。ぜひ楽しみながら挑戦してみてください。

この記事で学んだこと

この記事では、TypeScriptのジェネリクス型定義の基本と、React × TypeScriptを使ったアプリ開発まで、一通りの流れを学びました。最後に簡単に振り返りましょう。

  • ジェネリクスの基本

    ジェネリクスは、型を関数やクラスに「引数」として渡すことで、柔軟で型安全なコードを実現する仕組みです。

  • ジェネリクスの応用

    ジェネリクスをさらに深く理解するため、以下の応用的な概念を学びました。

    • 型パラメーターの種類
    • 型パラメーターのextendsによる制約
    • 型パラメーターのデフォルト値
    • 複数の型パラメーターの指定
  • React × TypeScript でのジェネリクス型定義

    ReactとTypeScriptを組み合わせ、ジェネリクスを活用したPropsやuseStateの型定義を学びました。

  • ジェネリクスを使った実践ハンズオン

    最後に、Todoアプリを作成するハンズオンを通して、ジェネリクスを実際のReactアプリ開発にどのように活用するかを段階的に理解することができました。

この記事を通して、ジェネリクスの理解が深まり、実際に活用できる自信がついていれば幸いです。プログラミングは、学べば学ぶほど新しい発見があり、できることがどんどん広がっていきます。それを楽しみながら学び続けることで、スキルは確実に成長していきます。焦らず、自分のペースで学習し、新しい挑戦をしながら進んでいきましょう。

参考資料

以下のリンクは、この記事で説明した手順や概念に関連する参考資料です。より詳しく学びたい方は、ぜひご覧ください。

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事