TypeScriptのGenerics(ジェネリクス)は、型を柔軟に扱うための強力な機能です。特にReactでコンポーネントを作成する際、再利用性の高いコードを書くために重要な役割を果たします。この記事では、Genericsの基本的な概念から、実践的なコンポーネント作成までを解説します。

Genericsとは

Genericsは型の一般化を可能にする機能で、コードの再利用性を高めることができます。型パラメータを使用することで、複数の型で動作する関数やクラスを作成できます。

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

// 使用例
const numberResult = identity<number>(42);    // number型
const stringResult = identity<string>("Hello"); // string型

ReactコンポーネントでのGenericsの活用

Reactでは、プロップスの型を柔軟に定義するためにGenericsを使用できます。以下は基本的な例です:

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>
    );
}

// 使用例
const App = () => {
    const numbers = [1, 2, 3, 4, 5];
    return (
        <List
            items={numbers}
            renderItem={(number) => <span>{number * 2}</span>}
        />
    );
};

制約付きGenerics

特定の型や型の特徴を持つものに限定したい場合は、extendsキーワードを使用します:

interface HasId {
    id: number;
}

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

// 使用例
interface User extends HasId {
    name: string;
    email: string;
}

const users: User[] = [
    { id: 1, name: "John", email: "john@example.com" },
    { id: 2, name: "Jane", email: "jane@example.com" }
];

const user = findById(users, 1);

実践的なコンポーネント例

データ取得と表示を行う汎用的なコンポーネントの例:

interface FetchContainerProps<T> {
    url: string;
    render: (data: T) => React.ReactNode;
}

function FetchContainer<T>({ url, render }: FetchContainerProps<T>) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        fetch(url)
            .then(response => response.json())
            .then(data => {
                setData(data);
                setLoading(false);
            })
            .catch(error => {
                setError(error);
                setLoading(false);
            });
    }, [url]);

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    if (!data) return null;

    return <>{render(data)}</>;
}

// 使用例
interface Post {
    id: number;
    title: string;
    body: string;
}

const PostComponent = () => (
    <FetchContainer<Post>
        url="https://api.example.com/posts/1"
        render={(post) => (
            <article>
                <h1>{post.title}</h1>
                <p>{post.body}</p>
            </article>
        )}
    />
);

ベストプラクティス

1. 型パラメータには意味のある名前をつける(T, U, V ではなく、TData, TProps など)

2. 必要な場合のみGenericsを使用し、過度な抽象化を避ける

3. 適切な制約を設定し、型の安全性を確保する

4. デフォルトの型パラメータを活用する

interface DataContainerProps<TData = any> {
    data: TData;
    loading?: boolean;
}

const DataContainer = <TData = any,>({ data, loading }: DataContainerProps<TData>) => {
    if (loading) return <div>Loading...</div>;
    return <div>{JSON.stringify(data)}</div>;
};

Genericsを使いこなすことで、型安全で再利用可能なコンポーネントを作成できます。適切に活用することで、コードの保守性と再利用性が大幅に向上します。