logo

鎌イルカのクラフト記

article icon

React勉強会資料 TypeScript型定義編

はじめに

これまでの勉強会では、ReactのコードをJavaScriptで書いてきました。しかし、実際のプロジェクトではTypeScriptを使うことが一般的になっています。

TypeScriptを使うことで:

  • バグを早期に発見できる(コンパイル時にエラーを検出)
  • コードの意図が明確になる(型がドキュメントの役割)
  • リファクタリングが安全になる(影響範囲を把握できる)
  • IDEの補完が強力になる(自動補完、型情報の表示)

今回は、TypeScriptの基本から、Reactでの型定義、型ドリブン開発の実践まで学んでいきます。

なぜTypeScriptを使うのか

JavaScriptとTypeScriptの違いを見てみましょう。

// JavaScript - 型がない
function greet(name) {
  return `Hello, ${name}!`;
}
 
greet('Alice');      // OK
greet(123);          // 実行時まで問題に気づかない
greet();             // 実行時まで問題に気づかない
greet(null);         // 実行時エラーの可能性
// TypeScript - 型がある
function greet(name: string): string {
  return `Hello, ${name}!`;
}
 
greet('Alice');      // OK
greet(123);          // ❌ コンパイルエラー
greet();             // ❌ コンパイルエラー
greet(null);         // ❌ コンパイルエラー

型があることで、実行前にエラーを発見できます。

Reactコンポーネントでの例

// JavaScript - propsの型が不明
function UserCard({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.emal}</p> {/* タイポ!でも気づかない */}
    </div>
  );
}
// TypeScript - propsの型を定義
interface UserCardProps {
  user: {
    name: string;
    email: string;
    age: number;
  };
}
 
function UserCard({ user }: UserCardProps) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.emal}</p> {/* ❌ コンパイルエラー: 'emal'は存在しない */}
    </div>
  );
}

TypeScriptの基本的な型

プリミティブ型

// 基本的な型
const name: string = 'Alice';
const age: number = 25;
const isActive: boolean = true;
const data: null = null;
const value: undefined = undefined;
 
// 配列
const numbers: number[] = [1, 2, 3];
const names: string[] = ['Alice', 'Bob'];
const mixed: (string | number)[] = ['Alice', 25]; // Union型

オブジェクト型

// オブジェクトの型定義
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;        // オプショナル(?を付ける)
  readonly role: string; // 読み取り専用
}
 
const user: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  role: 'admin'
};
 
user.name = 'Bob';     // OK
user.role = 'user';    // ❌ エラー: readonlyは変更できない

関数の型

// 関数の引数と戻り値の型
function add(a: number, b: number): number {
  return a + b;
}
 
// アロー関数
const multiply = (a: number, b: number): number => {
  return a * b;
};
 
// 戻り値がない関数
function log(message: string): void {
  console.log(message);
}
 
// オプショナル引数
function greet(name: string, greeting?: string): string {
  return `${greeting || 'Hello'}, ${name}!`;
}

Union型とIntersection型

// Union型: AまたはB
type Status = 'success' | 'error' | 'loading';
 
function handleStatus(status: Status) {
  if (status === 'success') {
    console.log('成功');
  }
  // status === 'pending' // ❌ エラー: 'pending'は存在しない
}
 
// Intersection型: AかつB
interface Person {
  name: string;
  age: number;
}
 
interface Employee {
  employeeId: string;
  department: string;
}
 
type EmployeePerson = Person & Employee;
 
const employee: EmployeePerson = {
  name: 'Alice',
  age: 25,
  employeeId: 'E001',
  department: 'Engineering'
};

Reactコンポーネントの型定義

関数コンポーネントの型定義

// 方法1: Propsの型を定義
interface ButtonProps {
  children: React.ReactNode;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}
 
function Button({ children, onClick, variant = 'primary', disabled }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {children}
    </button>
  );
}
 
// 方法2: React.FCを使う(現在は非推奨)
const Button: React.FC<ButtonProps> = ({ children, onClick, variant = 'primary', disabled }) => {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  );
};

イベントハンドラの型

function Form() {
  // インプットのイベント
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
 
  // テキストエリアのイベント
  const handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    console.log(e.target.value);
  };
 
  // フォーム送信
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('送信');
  };
 
  // クリックイベント
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('クリック');
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <textarea onChange={handleTextAreaChange} />
      <button onClick={handleClick}>送信</button>
    </form>
  );
}

Hooksの型定義

// useState
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
 
// useRef
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
 
// useContext
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}
 
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
 
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useThemeはThemeProvider内で使用してください');
  }
  return context;
}

childrenの型

// ReactNodeを使うのが最も柔軟
interface LayoutProps {
  children: React.ReactNode;
}
 
// 特定の要素だけを受け入れる
interface ListProps {
  children: React.ReactElement<ItemProps> | React.ReactElement<ItemProps>[];
}
 
// JSX要素のみ
interface ContainerProps {
  children: JSX.Element;
}
 
// 複数の子要素
interface WrapperProps {
  children: React.ReactNode[];
}

型推論:TypeScriptに型を推測させる

TypeScriptは多くの場合、型を自動的に推測できます。

// 型推論: TypeScriptが自動的に型を推測
const name = 'Alice';        // string型と推測
const age = 25;              // number型と推測
const isActive = true;       // boolean型と推測
 
// 配列の型推論
const numbers = [1, 2, 3];   // number[]型と推測
const mixed = [1, 'two', 3]; // (string | number)[]型と推測
 
// オブジェクトの型推論
const user = {
  name: 'Alice',
  age: 25
}; // { name: string; age: number; }型と推測
 
// 関数の戻り値の型推論
function add(a: number, b: number) {
  return a + b; // number型と推測
}
 
// Reactコンポーネントでの型推論
function Counter() {
  const [count, setCount] = useState(0); // number型と推測
 
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>増やす</button>
    </div>
  );
}

型推論を活用することで、コードがシンプルになります。ただし、明示的な型定義が必要な場合もあります。

// 型推論では不十分な例
const [user, setUser] = useState(null); // null型と推測されてしまう
 
// 明示的に型を指定
const [user, setUser] = useState<User | null>(null); // User | null型

型ガード:型を絞り込む

型ガードを使うと、条件分岐で型を絞り込めます。

typeof型ガード

function processValue(value: string | number) {
  if (typeof value === 'string') {
    // この中では value は string型
    console.log(value.toUpperCase());
  } else {
    // この中では value は number型
    console.log(value.toFixed(2));
  }
}

instanceof型ガード

class Dog {
  bark() {
    console.log('ワンワン');
  }
}
 
class Cat {
  meow() {
    console.log('ニャー');
  }
}
 
function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // Dogのメソッドが使える
  } else {
    animal.meow(); // Catのメソッドが使える
  }
}

in型ガード

interface Bird {
  fly: () => void;
  layEggs: () => void;
}
 
interface Fish {
  swim: () => void;
  layEggs: () => void;
}
 
function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    animal.fly(); // Birdのメソッドが使える
  } else {
    animal.swim(); // Fishのメソッドが使える
  }
}

ユーザー定義型ガード

interface User {
  id: number;
  name: string;
  email: string;
}
 
interface Admin {
  id: number;
  name: string;
  email: string;
  role: 'admin';
}
 
// 型ガード関数
function isAdmin(user: User | Admin): user is Admin {
  return 'role' in user && user.role === 'admin';
}
 
function greetUser(user: User | Admin) {
  if (isAdmin(user)) {
    // この中では user は Admin型
    console.log(`管理者: ${user.name}, 権限: ${user.role}`);
  } else {
    // この中では user は User型
    console.log(`ユーザー: ${user.name}`);
  }
}

Reactでの型ガードの活用

interface SuccessState {
  status: 'success';
  data: string[];
}
 
interface ErrorState {
  status: 'error';
  error: string;
}
 
interface LoadingState {
  status: 'loading';
}
 
type ApiState = SuccessState | ErrorState | LoadingState;
 
function ApiComponent({ state }: { state: ApiState }) {
  if (state.status === 'loading') {
    return <div>読み込み中...</div>;
  }
 
  if (state.status === 'error') {
    // この中では state.error にアクセス可能
    return <div>エラー: {state.error}</div>;
  }
 
  // この中では state.data にアクセス可能
  return (
    <ul>
      {state.data.map((item, i) => (
        <li key={i}>{item}</li>
      ))}
    </ul>
  );
}

ジェネリクス:型を抽象化する

ジェネリクスを使うと、再利用可能な型安全なコードが書けます。

基本的なジェネリクス

// ジェネリクスなし: 型ごとに関数を書く必要がある
function getFirstNumber(arr: number[]): number {
  return arr[0];
}
 
function getFirstString(arr: string[]): string {
  return arr[0];
}
 
// ジェネリクスあり: 1つの関数で全ての型に対応
function getFirst<T>(arr: T[]): T {
  return arr[0];
}
 
const firstNumber = getFirst([1, 2, 3]);      // number型
const firstName = getFirst(['a', 'b', 'c']);  // string型
const firstBool = getFirst([true, false]);    // boolean型

Reactコンポーネントでのジェネリクス

// 汎用的なリストコンポーネント
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;
}
 
function UserList() {
  const users: User[] = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];
 
  return (
    <List
      items={users}
      renderItem={(user) => (
        <div>
          {user.id}: {user.name}
        </div>
      )}
    />
  );
}

カスタムフックでのジェネリクス

// 汎用的なデータ取得フック
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then((data: T) => {
        setData(data);
        setLoading(false);
      })
      .catch((err: Error) => {
        setError(err);
        setLoading(false);
      });
  }, [url]);
 
  return { data, loading, error };
}
 
// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}
 
function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
 
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;
 
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

ジェネリクスの制約

// lengthプロパティを持つ型のみ受け入れる
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}
 
getLength('hello');        // OK: stringはlengthを持つ
getLength([1, 2, 3]);      // OK: 配列はlengthを持つ
getLength({ length: 10 }); // OK: lengthプロパティがある
getLength(123);            // ❌ エラー: numberはlengthを持たない

Utility Types:便利な型変換

TypeScriptには組み込みのUtility Typesがあります。

Partial - 全てのプロパティをオプショナルに

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}
 
// 一部のプロパティだけを更新する関数
function updateUser(id: number, updates: Partial<User>) {
  // updatesは全てのプロパティがオプショナル
  console.log(`ユーザー${id}を更新:`, updates);
}
 
updateUser(1, { name: 'Alice' });              // OK
updateUser(2, { email: 'bob@example.com' });   // OK
updateUser(3, { name: 'Charlie', age: 30 });   // OK

Required - 全てのプロパティを必須に

interface Config {
  host?: string;
  port?: number;
  protocol?: 'http' | 'https';
}
 
// 全てのプロパティを必須にする
type RequiredConfig = Required<Config>;
 
const config: RequiredConfig = {
  host: 'localhost',
  port: 3000,
  protocol: 'http'
  // どれか1つでも欠けているとエラー
};

Readonly - 全てのプロパティを読み取り専用に

interface User {
  id: number;
  name: string;
}
 
const user: Readonly<User> = {
  id: 1,
  name: 'Alice'
};
 
user.name = 'Bob'; // ❌ エラー: 読み取り専用

Pick - 特定のプロパティだけを抽出

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  address: string;
}
 
// nameとemailだけを抽出
type UserPreview = Pick<User, 'name' | 'email'>;
 
const preview: UserPreview = {
  name: 'Alice',
  email: 'alice@example.com'
  // id, age, addressは不要
};

Omit - 特定のプロパティを除外

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}
 
// passwordを除外
type UserWithoutPassword = Omit<User, 'password'>;
 
const user: UserWithoutPassword = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
  // passwordは含まれない
};

Record - キーと値の型を指定

// キーがstring、値がnumberのオブジェクト
type Scores = Record<string, number>;
 
const scores: Scores = {
  math: 90,
  english: 85,
  science: 92
};
 
// より厳密な型定義
type Status = 'pending' | 'approved' | 'rejected';
type StatusCounts = Record<Status, number>;
 
const counts: StatusCounts = {
  pending: 5,
  approved: 10,
  rejected: 2
};

型ドリブン開発の実践

型を先に定義してから実装する開発手法です。

1. まず型を定義する

// APIのレスポンス型を定義
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}
 
interface ApiResponse<T> {
  data: T;
  meta: {
    total: number;
    page: number;
    perPage: number;
  };
}
 
// コンポーネントのProps型を定義
interface UserListProps {
  onUserSelect: (user: User) => void;
  filter?: 'active' | 'inactive';
}

2. 型に沿って実装する

// 型定義に従って実装
function UserList({ onUserSelect, filter }: UserListProps) {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetch(`/api/users?filter=${filter || 'active'}`)
      .then(res => res.json())
      .then((response: ApiResponse<User[]>) => {
        setUsers(response.data);
        setLoading(false);
      });
  }, [filter]);
 
  if (loading) return <div>読み込み中...</div>;
 
  return (
    <ul>
      {users.map(user => (
        <li key={user.id} onClick={() => onUserSelect(user)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

3. 型のメリット

// ✅ IDEの補完が効く
function handleUser(user: User) {
  console.log(user.); // name, email, id, createdAtが候補に出る
}
 
// ✅ タイポをコンパイル時に検出
function printUser(user: User) {
  console.log(user.emal); // ❌ エラー: 'emal'は存在しない
}
 
// ✅ リファクタリングが安全
// Userのプロパティを変更すると、使用箇所全てでエラーが出る
interface User {
  id: number;
  name: string;
  email: string;
  // createdAtを削除
}
 
function formatDate(user: User) {
  return user.createdAt; // ❌ エラー: 'createdAt'は存在しない
}

実践例:型安全なフォーム

// フォームの値の型
interface FormData {
  name: string;
  email: string;
  age: number;
  terms: boolean;
}
 
// バリデーションエラーの型
type FormErrors = Partial<Record<keyof FormData, string>>;
 
function RegistrationForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    age: 0,
    terms: false
  });
 
  const [errors, setErrors] = useState<FormErrors>({});
 
  // 型安全な更新関数
  const updateField = <K extends keyof FormData>(
    field: K,
    value: FormData[K]
  ) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  };
 
  const validate = (): boolean => {
    const newErrors: FormErrors = {};
 
    if (!formData.name) {
      newErrors.name = '名前は必須です';
    }
 
    if (!formData.email.includes('@')) {
      newErrors.email = '有効なメールアドレスを入力してください';
    }
 
    if (formData.age < 18) {
      newErrors.age = '18歳以上である必要があります';
    }
 
    if (!formData.terms) {
      newErrors.terms = '利用規約に同意してください';
    }
 
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
 
    if (validate()) {
      console.log('送信:', formData);
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          value={formData.name}
          onChange={(e) => updateField('name', e.target.value)}
        />
        {errors.name && <span>{errors.name}</span>}
      </div>
 
      <div>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => updateField('email', e.target.value)}
        />
        {errors.email && <span>{errors.email}</span>}
      </div>
 
      <div>
        <input
          type="number"
          value={formData.age}
          onChange={(e) => updateField('age', Number(e.target.value))}
        />
        {errors.age && <span>{errors.age}</span>}
      </div>
 
      <div>
        <input
          type="checkbox"
          checked={formData.terms}
          onChange={(e) => updateField('terms', e.target.checked)}
        />
        <label>利用規約に同意する</label>
        {errors.terms && <span>{errors.terms}</span>}
      </div>
 
      <button type="submit">登録</button>
    </form>
  );
}

よくあるTypeScriptのアンチパターン

1. any型の乱用

// ❌ 悪い例: any型を使う
function processData(data: any) {
  return data.value.toUpperCase(); // ランタイムエラーの可能性
}
 
// ✅ 良い例: 適切な型を定義
interface Data {
  value: string;
}
 
function processData(data: Data) {
  return data.value.toUpperCase(); // 型安全
}

2. 型アサーションの乱用

// ❌ 悪い例: 型アサーションで強制的に型を変える
const user = {} as User; // 空のオブジェクトをUserとして扱う(危険)
 
// ✅ 良い例: 適切に値を設定する
const user: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
};

3. 型定義の重複

// ❌ 悪い例: 同じような型を何度も定義
interface UserResponse {
  id: number;
  name: string;
  email: string;
}
 
interface UserData {
  id: number;
  name: string;
  email: string;
}
 
// ✅ 良い例: 1つの型を再利用
interface User {
  id: number;
  name: string;
  email: string;
}
 
type UserResponse = User;
type UserData = User;

まとめ

TypeScriptで型安全なReactアプリケーションを構築するポイント:

基本

  • プリミティブ型: string, number, boolean
  • オブジェクト型: interface, type
  • Union型: A | B(AまたはB)
  • Intersection型: A & B(AかつB)

React固有の型

  • コンポーネントProps: interfaceで定義
  • イベントハンドラ: React.ChangeEvent, React.MouseEvent
  • Hooks: useState<T>, useRef<T>
  • children: React.ReactNode

高度な型機能

  • 型推論: TypeScriptに型を推測させる
  • 型ガード: typeof, instanceof, in, ユーザー定義
  • ジェネリクス: 再利用可能な型安全なコード
  • Utility Types: Partial, Required, Pick, Omit, Record

型ドリブン開発

  1. まず型を定義する
  2. 型に沿って実装する
  3. コンパイルエラーで安全性を確保

TypeScriptを使いこなすことで、バグの少ない保守性の高いReactアプリケーションを構築できます!

次のステップ: ここまでで基礎的な内容は一通り学びました。次は以下のような応用的なトピックに挑戦してみましょう:

  • パフォーマンス最適化(useMemo、useCallback、React 19の新機能)
  • 実践的なアプリケーション構築(状態管理ライブラリ、ルーティング)
  • テスト(Jest、React Testing Library)
  • アーキテクチャ編(実際にコードを書いた経験を積んでから読むと理解が深まります)

参考リンク

The starting point for learning TypeScriptFind TypeScript starter projects: from Angular to React or Node.js and CLIs.
favicon of www.typescriptlang.org
React TypeScript Cheatsheets | React TypeScript CheatsheetsReact TypeScript Cheatsheets
favicon of https://react-typescript-cheatsheet.netlify.app/react-typescript-cheatsheet.netlify.app
ogp of https://user-images.githubusercontent.com/6764957/53868378-2b51fc80-3fb3-11e9-9cee-0277efe8a927.png

目次