ジェネリクス型って何?
ジェネリクス型(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: こんな時に使います:
- 同じロジックを複数の型で使いたい時
function swap<T>(a: T, b: T): [T, T] {
return [b, a];
}
- 型安全なコレクションを作りたい時
class Stack<T> {
private items: T[] = [];
push(item: T) { ... }
pop(): T | undefined { ... }
}
- 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;
}
覚えておくこと
-
で型パラメータを宣言
- 好きな名前でOK(T, U, V がよく使われる)
-
使う時に型が決まる
-
関数名<string>(...)のように指定 - 推論されることも多い
-
-
extendsで制約をつけられる
<T extends SomeType>
-
複数の型パラメータも使える
<T, U, V>
最も大事なこと:
ジェネリクスは「型の変数」。関数が値を受け取るように、ジェネリクスは型を受け取る!