1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ジェネリクス型とは? - 中学生でもわかる解説

Posted at

ジェネリクス型って何?

ジェネリクス型(Generics)は、「後から型を決められる」魔法の箱です。

簡単に言うと

普通の関数は「特定の型」しか扱えません。
ジェネリクスを使うと「どんな型でも扱える、でも型安全」な関数が作れます。

// 普通の関数(数値だけ)
function getFirstNumber(items: number[]): number {
  return items[0];
}

// ジェネリクス関数(何でもOK)
function getFirst<T>(items: T[]): T {
  return items[0];
}

なぜ必要なの?

問題:型ごとに同じ関数を書くのは大変

こんなコードを書いたことありませんか?

// 数値の配列から最初の要素を取る
function getFirstNumber(items: number[]): number {
  return items[0];
}

// 文字列の配列から最初の要素を取る
function getFirstString(items: string[]): string {
  return items[0];
}

// ブール値の配列から最初の要素を取る
function getFirstBoolean(items: boolean[]): boolean {
  return items[0];
}

めんどくさい! やってることは全部同じなのに...

解決:ジェネリクスで1つにまとめる

function getFirst<T>(items: T[]): T {
  return items[0];
}

// どの型でも使える!
const firstNumber = getFirst([1, 2, 3]);        // number
const firstString = getFirst(['a', 'b', 'c']); // string
const firstBoolean = getFirst([true, false]);   // boolean

たった1つの関数で、全ての型に対応!

基本的な使い方

基本の書き方

function 関数名<T>(引数: T): T {
  return 引数;
}

ポイント:

  • <T> が「型の箱」
  • T は好きな名前でOK(T, U, V がよく使われる)
  • 使う時に型が決まる

例1:値をそのまま返す関数

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

// 使う時に型が決まる
const num = identity(123);        // numは number型
const str = identity("hello");    // strは string型
const bool = identity(true);      // boolは boolean型

例2:配列の最後の要素を取得

function getLast<T>(items: T[]): T {
  return items[items.length - 1];
}

const lastNumber = getLast([1, 2, 3]);           // 3 (number型)
const lastString = getLast(['a', 'b', 'c']);    // 'c' (string型)

例3:配列を逆順にする

function reverse<T>(items: T[]): T[] {
  return items.reverse();
}

const numbers = reverse([1, 2, 3]);        // [3, 2, 1]
const strings = reverse(['a', 'b', 'c']); // ['c', 'b', 'a']

実際の使用例

例1:APIレスポンスの型定義

// ジェネリクスでAPIレスポンスの型を定義
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message: string;
}

// ユーザー情報のレスポンス
type UserResponse = ApiResponse<{
  id: number;
  name: string;
  email: string;
}>;

// 商品情報のレスポンス
type ProductResponse = ApiResponse<{
  id: number;
  name: string;
  price: number;
}>;

// 使用例
const userRes: UserResponse = {
  success: true,
  data: {
    id: 1,
    name: "太郎",
    email: "taro@example.com"
  },
  message: "取得成功"
};

例2:fetch関数のラッパー

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  const data = await response.json();
  return data as T;
}

// 型を指定して使う
interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

// ユーザーデータを取得
const user = await fetchData<User>('https://api.example.com/user/1');
console.log(user.name); // 型安全!

// 商品データを取得
const product = await fetchData<Product>('https://api.example.com/products/1');
console.log(product.price); // 型安全!

例3:状態管理

interface State<T> {
  data: T;
  loading: boolean;
  error: string | null;
}

// ユーザー情報の状態
type UserState = State<{
  id: number;
  name: string;
}>;

// 商品リストの状態
type ProductListState = State<Array<{
  id: number;
  name: string;
  price: number;
}>>;

// 使用例
const userState: UserState = {
  data: { id: 1, name: "太郎" },
  loading: false,
  error: null
};

const productState: ProductListState = {
  data: [
    { id: 1, name: "商品A", price: 1000 },
    { id: 2, name: "商品B", price: 2000 }
  ],
  loading: false,
  error: null
};

Reactでの使用例

例1:useState

import { useState } from 'react';

function UserProfile() {
  // ジェネリクスで型を指定
  const [name, setName] = useState<string>('');
  const [age, setAge] = useState<number>(0);
  const [user, setUser] = useState<{
    id: number;
    name: string;
    email: string;
  } | null>(null);
  
  return (
    <div>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
      />
      <input 
        type="number"
        value={age} 
        onChange={(e) => setAge(Number(e.target.value))} 
      />
    </div>
  );
}

例2:カスタムフック

// ジェネリクスを使ったカスタムフック
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });
  
  const setStoredValue = (newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };
  
  return [value, setStoredValue] as const;
}

// 使用例
function App() {
  // 文字列を保存
  const [name, setName] = useLocalStorage<string>('name', '');
  
  // 数値を保存
  const [count, setCount] = useLocalStorage<number>('count', 0);
  
  // オブジェクトを保存
  const [user, setUser] = useLocalStorage<{
    id: number;
    name: string;
  }>('user', { id: 0, name: '' });
  
  return <div>...</div>;
}

例3:コンポーネントのprops

// ジェネリクスを使ったリストコンポーネント
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 使用例
interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

function App() {
  const users: User[] = [
    { id: 1, name: "太郎" },
    { id: 2, name: "花子" }
  ];
  
  const products: Product[] = [
    { id: 1, name: "商品A", price: 1000 },
    { id: 2, name: "商品B", price: 2000 }
  ];
  
  return (
    <div>
      <h2>ユーザー一覧</h2>
      <List 
        items={users} 
        renderItem={(user) => <span>{user.name}</span>}
      />
      
      <h2>商品一覧</h2>
      <List 
        items={products} 
        renderItem={(product) => (
          <span>{product.name} - {product.price}</span>
        )}
      />
    </div>
  );
}

Next.jsでの使用例

例1:動的ルーティングでのデータ取得

// app/products/[id]/page.tsx
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

async function fetchProduct<T>(id: string): Promise<T> {
  const response = await fetch(`https://api.shop.com/products/${id}`);
  return response.json();
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await fetchProduct<Product>(params.id);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      <p>{product.description}</p>
    </div>
  );
}

例2:API Routeでの使用

// app/api/data/route.ts
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

export async function GET(request: Request) {
  try {
    const data = await fetchDataFromDatabase();
    
    const response: ApiResponse<typeof data> = {
      success: true,
      data: data
    };
    
    return Response.json(response);
  } catch (error) {
    const response: ApiResponse<null> = {
      success: false,
      data: null,
      error: 'データの取得に失敗しました'
    };
    
    return Response.json(response, { status: 500 });
  }
}

例3:再利用可能なデータフェッチ関数

// lib/api.ts
interface FetchOptions {
  cache?: RequestCache;
  revalidate?: number;
}

async function fetchApi<T>(
  endpoint: string, 
  options?: FetchOptions
): Promise<T> {
  const response = await fetch(`https://api.example.com${endpoint}`, {
    cache: options?.cache,
    next: { revalidate: options?.revalidate }
  });
  
  if (!response.ok) {
    throw new Error('データの取得に失敗しました');
  }
  
  return response.json();
}

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
}

export async function getUser(id: string): Promise<User> {
  return fetchApi<User>(`/users/${id}`, { revalidate: 60 });
}

export async function getPosts(): Promise<Post[]> {
  return fetchApi<Post[]>('/posts', { cache: 'no-store' });
}

複数の型パラメータ

基本の使い方

// 2つの型パラメータ
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result1 = pair(1, "hello");      // [number, string]
const result2 = pair(true, 123);       // [boolean, number]
const result3 = pair("a", "b");        // [string, string]

例:key-valueペア

interface KeyValue<K, V> {
  key: K;
  value: V;
}

const item1: KeyValue<string, number> = {
  key: "age",
  value: 25
};

const item2: KeyValue<number, string> = {
  key: 1,
  value: "太郎"
};

const item3: KeyValue<string, { id: number; name: string }> = {
  key: "user",
  value: { id: 1, name: "太郎" }
};

例:Map関数

function mapArray<T, U>(
  items: T[], 
  transform: (item: T) => U
): U[] {
  return items.map(transform);
}

// 数値を文字列に変換
const strings = mapArray([1, 2, 3], (num) => num.toString());
// ["1", "2", "3"] (string[])

// 文字列の長さを取得
const lengths = mapArray(["a", "bb", "ccc"], (str) => str.length);
// [1, 2, 3] (number[])

// オブジェクトから特定のプロパティを抽出
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: "太郎" },
  { id: 2, name: "花子" }
];

const names = mapArray(users, (user) => user.name);
// ["太郎", "花子"] (string[])

制約をつける

extendsで型を制限

特定の条件を満たす型だけを受け付けることができます。

// lengthプロパティを持つ型だけ受け付ける
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello");        // OK (文字列はlengthを持つ)
getLength([1, 2, 3]);      // OK (配列はlengthを持つ)
getLength({ length: 5 });  // OK
// getLength(123);         // エラー!数値はlengthを持たない

例:IDを持つオブジェクト

// idプロパティを持つオブジェクトだけ受け付ける
interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

const users: User[] = [
  { id: 1, name: "太郎" },
  { id: 2, name: "花子" }
];

const products: Product[] = [
  { id: 1, name: "商品A", price: 1000 },
  { id: 2, name: "商品B", price: 2000 }
];

const user = findById(users, 1);       // User型
const product = findById(products, 2); // Product型

例:比較可能な型

function max<T extends number | string>(a: T, b: T): T {
  return a > b ? a : b;
}

console.log(max(10, 20));        // 20
console.log(max("a", "z"));      // "z"
// console.log(max(true, false)); // エラー!booleanは許可されていない

よくあるパターン

パターン1:非同期データフェッチ

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

async function useFetch<T>(url: string): Promise<FetchState<T>> {
  const state: FetchState<T> = {
    data: null,
    loading: true,
    error: null
  };
  
  try {
    const response = await fetch(url);
    const data = await response.json();
    state.data = data;
    state.loading = false;
  } catch (error) {
    state.error = error as Error;
    state.loading = false;
  }
  
  return state;
}

// 使用例
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

const todoState = await useFetch<Todo>('https://api.example.com/todos/1');

if (todoState.data) {
  console.log(todoState.data.title);
}

パターン2:配列の操作

// 配列をフィルタリング
function filter<T>(
  items: T[], 
  predicate: (item: T) => boolean
): T[] {
  return items.filter(predicate);
}

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filter(numbers, (n) => n % 2 === 0);
// [2, 4]

const users = [
  { id: 1, name: "太郎", age: 20 },
  { id: 2, name: "花子", age: 25 },
  { id: 3, name: "次郎", age: 30 }
];
const adults = filter(users, (user) => user.age >= 25);
// [{ id: 2, name: "花子", age: 25 }, { id: 3, name: "次郎", age: 30 }]

パターン3:キャッシュ

class Cache<T> {
  private data = new Map<string, T>();
  
  set(key: string, value: T): void {
    this.data.set(key, value);
  }
  
  get(key: string): T | undefined {
    return this.data.get(key);
  }
  
  has(key: string): boolean {
    return this.data.has(key);
  }
  
  clear(): void {
    this.data.clear();
  }
}

// ユーザー情報のキャッシュ
interface User {
  id: number;
  name: string;
}

const userCache = new Cache<User>();
userCache.set('user1', { id: 1, name: "太郎" });
const user = userCache.get('user1');

// 商品情報のキャッシュ
interface Product {
  id: number;
  name: string;
  price: number;
}

const productCache = new Cache<Product>();
productCache.set('product1', { id: 1, name: "商品A", price: 1000 });

よくある質問

Q1: Tって何の略?

A: Type(型)の頭文字です。でも、好きな名前でOK!

// よく使われる名前
function example1<T>(value: T): T { ... }        // T (Type)
function example2<U>(value: U): U { ... }        // U (次の型)
function example3<K, V>(key: K, value: V) { ... } // K (Key), V (Value)

// 意味のある名前もOK
function example4<TUser>(user: TUser): TUser { ... }
function example5<Data>(data: Data): Data { ... }

Q2: ジェネリクスとanyの違いは?

A: ジェネリクスは型安全、anyは型チェックなし。

// anyを使った場合
function identityAny(value: any): any {
  return value;
}

const result1 = identityAny("hello");
result1.toUpperCase();  // OK だけど...
result1.notExist();     // エラーにならない!危険

// ジェネリクスを使った場合
function identityGeneric<T>(value: T): T {
  return value;
}

const result2 = identityGeneric("hello");
result2.toUpperCase();  // OK
// result2.notExist();  // エラー!存在しないメソッド

Q3: いつ使えばいい?

A: こんな時に使います:

  1. 同じロジックを複数の型で使いたい時
function swap<T>(a: T, b: T): [T, T] {
  return [b, a];
}
  1. 型安全なコレクションを作りたい時
class Stack<T> {
  private items: T[] = [];
  push(item: T) { ... }
  pop(): T | undefined { ... }
}
  1. API レスポンスの型を定義する時
interface ApiResponse<T> {
  data: T;
  status: number;
}

Q4: クラスでも使える?

A: はい、使えます!

class Box<T> {
  private value: T;
  
  constructor(value: T) {
    this.value = value;
  }
  
  getValue(): T {
    return this.value;
  }
  
  setValue(value: T): void {
    this.value = value;
  }
}

const numberBox = new Box(123);
console.log(numberBox.getValue()); // 123

const stringBox = new Box("hello");
console.log(stringBox.getValue()); // "hello"

まとめ

ジェネリクスの本質

「後から型を決められる」柔軟性と「型安全」を両立する仕組み

  • 1つのコードで複数の型に対応
  • 型安全性を保ちながら再利用可能
  • TypeScriptの強力な機能の1つ

基本の書き方

// 関数
function 関数名<T>(引数: T): T {
  return 引数;
}

// インターフェース
interface 名前<T> {
  data: T;
}

// クラス
class クラス名<T> {
  value: T;
}

覚えておくこと

  1. で型パラメータを宣言

    • 好きな名前でOK(T, U, V がよく使われる)
  2. 使う時に型が決まる

    • 関数名<string>(...)のように指定
    • 推論されることも多い
  3. extendsで制約をつけられる

    • <T extends SomeType>
  4. 複数の型パラメータも使える

    • <T, U, V>

最も大事なこと:
ジェネリクスは「型の変数」。関数が値を受け取るように、ジェネリクスは型を受け取る!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?