はじめに
前回のおさらい
前回の記事では、TypeScriptの基礎として以下の内容を学びました。
- TypeScriptの基本的な型(string, number, booleanなど)
- 関数の引数と返却値への型指定
- tsconfig.jsonの基本設定
今回は、それらの知識を活用して、React開発における実践的な型定義の方法を解説します。
この記事の対象読者と学べる内容
この記事は以下の方を対象としています。
- Reactの基本的な使い方を理解している方
- TypeScriptの基礎知識がある方
- React × TypeScriptの実践的な使い方を学びたい方
この記事では、以下の内容を学びます。
- API取得データの型定義によるバグ防止
- Reactコンポーネントにおける型定義のベストプラクティス
- 型定義の効率的な管理方法
- null安全なコードの書き方
取得データの型を定義しバグを防ぐ
APIレスポンスの型定義の重要性
外部APIからデータを取得する際、レスポンスの構造は実行時まで確定しません。型定義を行うことで、以下のメリットが得られます。
- プロパティ名の誤字を防ぐ
- 存在しないプロパティへのアクセスを防ぐ
- IDEの補完機能が使える
interfaceを使った型定義
APIから取得するデータの構造を interface で定義します。
Typeで定義してもよいのですが慣習でinterfaceになっていることが多いです。
// ユーザーデータの型定義
interface User {
id: number;
name: string;
email: string;
age: number;
}
// 投稿データの型定義
interface Post {
id: number;
title: string;
body: string;
userId: number;
createdAt: string;
}
オプショナルなプロパティには ? を使用します。
interface User {
id: number;
name: string;
email: string;
age?: number; // 省略可能
phoneNumber?: string; // 省略可能
}
実践例:ユーザーデータの取得
実際にAPIからデータを取得する例を見てみましょう。
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
age: number;
}
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('https://api.example.com/users');
const data: User[] = await response.json();
setUsers(data);
} catch (error) {
console.error('データの取得に失敗しました', error);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) {
return <div>読み込み中...</div>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
型定義を行うことで、user. と入力した時点でIDEが id、name、email、age のプロパティを提案してくれます。
Propsに型を定義しよう
Propsとは
Propsは、親コンポーネントから子コンポーネントへデータを渡すための仕組みです。TypeScriptでは、Propsに型を定義することで、誤ったデータの受け渡しを防げます。
Propsの型定義方法
Propsの型は interface または type で定義します。
// interfaceを使った定義
interface UserCardProps {
name: string;
age: number;
email: string;
}
function UserCard({ name, age, email }: UserCardProps) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>年齢: {age}歳</p>
<p>メール: {email}</p>
</div>
);
}
// 使用例
function App() {
return (
<UserCard
name="太郎"
age={25}
email="taro@example.com"
/>
);
}
オプショナルなPropsの扱い
必須ではないPropsには ? をつけます。
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // オプショナル
variant?: 'primary' | 'secondary'; // オプショナル
}
function Button({ label, onClick, disabled, variant }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={variant || 'primary'}
>
{label}
</button>
);
}
// 使用例
<Button label="送信" onClick={() => console.log('clicked')} />
<Button label="キャンセル" onClick={() => {}} disabled={true} />
デフォルト値の設定
デフォルト値を設定することで、Propsが渡されなかった場合の動作を定義できます。
interface CardProps {
title: string;
description: string;
backgroundColor?: string;
}
function Card({
title,
description,
backgroundColor = '#ffffff'
}: CardProps) {
return (
<div style={{ backgroundColor }}>
<h3>{title}</h3>
<p>{description}</p>
</div>
);
}
childrenを受け取る場合は、React.ReactNode 型を使用します。
interface ContainerProps {
children: React.ReactNode;
className?: string;
}
function Container({ children, className }: ContainerProps) {
return (
<div className={className}>
{children}
</div>
);
}
型定義を効率的に管理しよう
typeとinterfaceの使い分け
TypeScriptには type と interface の2つの型定義方法があります。
interfaceの特徴
// 宣言のマージが可能
interface User {
id: number;
name: string;
}
interface User {
email: string;
}
// 結果: User型はid, name, emailを持つ
// 継承が可能
interface Admin extends User {
role: string;
}
typeの特徴
// ユニオン型やタプル型を定義できる
type Status = 'pending' | 'success' | 'error';
type Point = [number, number];
// 交差型(Intersection Types)
type UserWithTimestamp = User & {
createdAt: string;
updatedAt: string;
};
使い分けの目安:
- オブジェクトの型定義には
interfaceを使用 - ユニオン型や複雑な型には
typeを使用
型定義ファイルの整理方法
プロジェクトが大きくなると、型定義の管理が重要になります。以下のようなディレクトリ構成がおすすめです。
src/
├── types/
│ ├── index.ts # 型定義のエクスポート
│ ├── user.ts # ユーザー関連の型
│ ├── post.ts # 投稿関連の型
│ └── common.ts # 共通の型
├── components/
└── pages/
user.ts の例
// src/types/user.ts
export interface User {
id: number;
name: string;
email: string;
age?: number;
}
export interface UserProfile extends User {
bio: string;
avatarUrl: string;
}
export type UserRole = 'admin' | 'user' | 'guest';
index.ts でまとめてエクスポート
// src/types/index.ts
export * from './user';
export * from './post';
export * from './common';
使用例
// コンポーネントからのインポート
import { User, UserProfile } from '@/types';
function UserDetail({ user }: { user: UserProfile }) {
// ...
}
共通型の再利用
複数のコンポーネントで使用する型は、共通化して再利用しましょう。
// src/types/common.ts
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
export interface PaginationParams {
page: number;
limit: number;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
totalPages: number;
}
使用例
import { ApiResponse, PaginatedResponse } from '@/types';
// ユーザー一覧のレスポンス型
type UsersResponse = ApiResponse<PaginatedResponse<User>>;
async function fetchUsers(): Promise<UsersResponse> {
const response = await fetch('/api/users');
return response.json();
}
コンポーネント自体の型定義
React.FCの使い方
React.FC(FunctionComponent)は、関数コンポーネントの型を定義する方法の一つです。
import React from 'react';
interface GreetingProps {
name: string;
message?: string;
}
const Greeting: React.FC<GreetingProps> = ({ name, message }) => {
return (
<div>
<h1>こんにちは、{name}さん</h1>
{message && <p>{message}</p>}
</div>
);
};
React.FC を使うメリット:
-
childrenが自動的に型に含まれる(React 18以降は明示的に定義が必要) -
displayNameなどのプロパティが使える
関数コンポーネントの型定義パターン
React.FC を使わない方法もあります。
interface ButtonProps {
label: string;
onClick: () => void;
children?: React.ReactNode;
}
function Button({ label, onClick, children }: ButtonProps) {
return (
<button onClick={onClick}>
{label}
{children}
</button>
);
}
または、返り値の型を明示することもできます。
function Button({ label, onClick, children }: ButtonProps): JSX.Element {
return (
<button onClick={onClick}>
{label}
{children}
</button>
);
}
どちらを使うべきか
現在のReact開発では、React.FC を使わない方法が推奨されることが多くなっています。
React.FCを使わない理由
- React 18以降、
childrenが自動的に含まれなくなった - より明示的な型定義ができる
- シンプルで分かりやすい
推奨パターン
interface CardProps {
title: string;
description: string;
children?: React.ReactNode; // 必要な場合は明示的に定義
}
function Card({ title, description, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<p>{description}</p>
{children}
</div>
);
}
export default Card;
オプショナルチェイニングでnull安全なコードを書く
オプショナルチェイニングとは
オプショナルチェイニング(?.)は、プロパティが存在するか分からない場合に安全にアクセスするための構文です。
従来の書き方
interface User {
name: string;
address?: {
city?: string;
zipCode?: string;
};
}
function getUserCity(user: User): string {
// 複数のチェックが必要
if (user.address && user.address.city) {
return user.address.city;
}
return '不明';
}
オプショナルチェイニングを使った書き方
function getUserCity(user: User): string {
return user.address?.city || '不明';
}
オプショナルチェイニングは、プロパティが undefined または null の場合、それ以降の評価を中断して undefined を返します。
Nullish Coalescingとの組み合わせ
Nullish Coalescing演算子(??)は、値が null または undefined の場合にデフォルト値を返します。
interface Product {
name: string;
price?: number;
discount?: number;
}
function calculatePrice(product: Product): number {
// priceがundefinedの場合は0、discountがundefinedの場合は0
const price = product.price ?? 0;
const discount = product.discount ?? 0;
return price - discount;
}
|| との違いに注意しましょう。
const value1 = 0;
console.log(value1 || 10); // 10(0はfalsyなので)
console.log(value1 ?? 10); // 0(0はnullでもundefinedでもない)
const value2 = '';
console.log(value2 || 'デフォルト'); // 'デフォルト'
console.log(value2 ?? 'デフォルト'); // ''
補足:ライブラリの型定義について
@typesパッケージとは
npmでインストールするJavaScriptライブラリの多くは、TypeScriptの型定義を含んでいません。そのようなライブラリを使用する際には、@types パッケージをインストールします。
# Reactの型定義
npm install --save-dev @types/react @types/react-dom
# Node.jsの型定義
npm install --save-dev @types/node
# Expressの型定義
npm install --save-dev @types/express
@types パッケージは、開発時にのみ必要なので --save-dev オプションを使用します。
DefinitelyTypedプロジェクト
@types パッケージは、DefinitelyTyped というコミュニティプロジェクトで管理されています。
多くの人気ライブラリの型定義が公開されているため、以下のコマンドで検索できます。
npm search @types/ライブラリ名
型定義がないライブラリの扱い方
型定義が提供されていないライブラリを使用する場合、いくつかの選択肢があります。
1. declare moduleで簡易的な型定義を追加
プロジェクトルートに src/types/modules.d.ts ファイルを作成します。
// src/types/modules.d.ts
declare module 'some-library' {
export function someFunction(arg: string): void;
export const someValue: number;
}
2. any型として扱う
型安全性は失われますが、とりあえず使用できます。
// src/types/modules.d.ts
declare module 'some-library';
// 使用時
import someThing from 'some-library';
// someThingはany型として扱われる
3. 自分で型定義を書く
ライブラリの使用頻度が高い場合は、詳細な型定義を作成します。
// src/types/my-library.d.ts
declare module 'my-library' {
export interface Config {
apiKey: string;
timeout?: number;
}
export class Client {
constructor(config: Config);
connect(): Promise<void>;
disconnect(): void;
}
export function createClient(config: Config): Client;
}
最近のライブラリは、型定義を内包していることが多くなっています。
{
"name": "some-library",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}
まとめ
この記事では、React × TypeScriptの実践的な使い方を学びました。
この記事で学んだこと
- APIレスポンスに型定義を行うことで、実行時エラーを防ぐ
- Propsに型を定義し、コンポーネント間のデータ受け渡しを安全にする
- 型定義ファイルを整理し、再利用可能な構造にする
-
React.FCを使わないシンプルな型定義が現在の推奨 - オプショナルチェイニング(
?.)とNullish Coalescing(??)でnull安全なコードを書く -
@typesパッケージを使ってJavaScriptライブラリに型定義を追加する