React と Stripe を使用した TypeScript による型安全な決済実装ガイド

React ストリープライブラリにおける TypeScript の導入と実装

ストリープ(Stripe)の決済機能を実装する際、React コンポーネントの開発には TypeScript を併用することで、型システムの恩恵を享受できます。これにより、実行時エラーを未然に防ぎ、開発効率とコードの信頼性を向上させることが可能です。本稿では、@stripe/stripe-js および @stripe/react-stripe-js を用いた、堅牢な決済フローの構築方法を解説します。

設計上のメリット

TypeScript を採用する主な利点は、型チェックがコンパイル段階で機能することです。Stripe の公式ライブラリは強力な型定義を提供しており、これを利用することで以下の点が改善されます。

  • 静的検証: 不正なプロパティやメソッド呼び出しを早期に検出可能
  • 補完機能: IDE での IntelliSense が効果的に機能し、API メンバへのアクセスが容易になる
  • 可読性向上: 明示的な型定義により、意図がコードから明確に伝わる
  • 保守性: リファクタリング時の影響範囲を特定しやすくなる

環境設定と初期構築

まずは必要な依存関係をインストールし、プロジェクト設定を整備します。

パッケージの準備

プロジェクトが以下のバージョンに対応していることを確認してください。

  • React: 16.8 以上
  • TypeScript: 3.5 以上

依存関係のインストール

NPM または Yarn を使用して、必要パッケージを取得します。

npm install @stripe/stripe-js @stripe/react-stripe-js --save
# または
yarn add @stripe/stripe-js @stripe/react-stripe-js

TypeScript コンパイラ設定

tsconfig.json において、JSX およびモジュール解像度の設定が必要です。以下に推奨される設定例を示します。

{
  "compilerOptions": {
    "target": "es6",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "esModuleInterop": true
  },
  "include": ["src"]
}

型の体系とコンテキスト管理

TypeScript 環境下では、すべての Stripe 関連要素に対して明確なインターフェース定義が存在します。これらは主にコアとなる Hook や Props に適用されます。

主要な型定義

ストリープの各種要素(CardElement, PaymentElement など)は、共通のプロパティセットと独自の設定オプションを持ちます。

  • ElementProps: クラス名や ID などの基本属性を含むベースインタフェース
  • Element Configuration: 各フォーム要素固有のオプション設定
  • Provider Context: アプリケーション全体で利用可能な Stripe インスタンスの提供

Stripe インスタンスの提供

アプリルートを囲む形で、ストリープのコンテキストを作成します。ここで取得した公開キーは環境変数から読み込むのが一般的です。

import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';

// 非同期でストリームインスタンスを作成
const stripeInstancePromise = loadStripe(process.env.PUBLISHABLE_KEY || '');

interface CheckoutConfig {
  currency: string;
  amount: number;
}

const options: CheckoutConfig = {
  currency: 'jpy',
  amount: 1000,
};

function App() {
  return (
    <Elements stripe={stripeInstancePromise} options={options}>
      <PaymentInterface />
    </Elements>
  );
}

決済フォームの実装パターン

実際の決済処理では、カスタムフックを使用することでロジックを分離し、型安全性を保ったまま状態管理を行います。

Hooks API を活用した処理

React Hook を使用することで、コンポーネントの状態遷移を効率的に扱えます。

import { useState } from 'react';
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';

function PaymentInterface() {
  // Stripe 関連のデータ取得
  const paymentTools = useStripe();
  const elements = useElements();
  
  // エラーメッセージ管理
  const [alertMessage, setAlertMessage] = useState<string | null>(null);

  const handleTransaction = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (!paymentTools || !elements) {
      return;
    }

    // フォーム送信前のバリデーション
    const submitData = await elements.submit();
    if (submitData.error) {
      setAlertMessage(submitData.message);
      return;
    }

    // サーバー側からクライアントシークレットを取得
    const serverResponse = await fetch('/api/create-payment-intent', { method: 'POST' });
    const payload = await serverResponse.json();

    // 決済完了処理
    const result = await paymentTools.confirmPayment({
      elements,
      clientSecret: payload.client_secret,
      confirmParams: {
        return_url: window.location.origin + '/success',
      },
    });

    if (result.error) {
      setAlertMessage(result.error.message);
    }
  };

  return (
    <form onSubmit={handleTransaction}>
      <PaymentElement />
      <button 
        type="submit" 
        disabled={!paymentTools || !elements}
        className="pay-btn"
      >
        購入を進める
      </button>
      {alertMessage && (
        <div role="alert" className="error-display">
          {alertMessage}
        </div>
      )}
    </form>
  );
}

要素ごとの設定制御

特定の要素については、その独自のオプションタイプを使用して詳細な動作制御が可能です。

const cardSettings = {
  style: {
    base: {
      fontSize: '16px',
      color: '#32325d',
    },
    invalid: {
      color: '#fa755a',
      iconColor: '#fa755a',
    },
  },
  defaultValues: {
    billingDetails: {
      name: 'テストユーザー',
      email: 'user@example.com',
    },
  },
} as const;

<CardElement options={cardSettings} />

高度なパターン:ユーティリティフック

重複するロジックを抽出し、再利用可能なカスタムフックを作成することで、コード品質を維持できます。

interface PaymentStatus {
  status: 'idle' | 'processing' | 'completed' | 'failed';
  errorMessage?: string;
}

export function usePaymentHandler(returnPath: string) {
  const tools = useStripe();
  const fields = useElements();
  const [state, setState] = useState<PaymentStatus>({ status: 'idle' });

  const executeOrder = async () => {
    if (!tools || !fields) {
      setState({ status: 'failed', errorMessage: 'システム未準備' });
      return;
    }

    setState({ status: 'processing' });

    try {
      // バリデーション実行
      const validateResult = await fields.submit();
      if (validateResult.error) throw validateResult.error;

      // シークレット取得
      const data = await fetch('/api/order').then(r => r.json());
      
      // 認証処理
      const res = await tools.confirmPayment({
        elements: fields,
        clientSecret: data.secret_key,
        confirmParams: { return_url: window.location.origin + returnPath },
      });

      if (res.error) throw res.error;

      setState({ status: 'completed' });
    } catch (err) {
      const msg = err instanceof Error ? err.message : '不明なエラー';
      setState({ status: 'failed', errorMessage: msg });
    }
  };

  return { state, executeOrder };
}

イベントハンドリングと型付け

カード入力中の変更などを監視する際は、対応するイベント型を使用します。

const onCardStateChange = (ev: stripeJs.StripeCardElementChangeEvent) => {
  if (ev.error) {
    setErrorText(ev.error.message);
  } else {
    setErrorText(null);
  }

  if (ev.complete) {
    console.log('カード情報は全て揃っています');
  }
};

<CardElement onChange={onCardStateChange} />

トラブルシューティング

モジュールが見つからない場合

Error: Cannot find module '@/stripe...' が発生する場合、依存関係が正しくリンクされていない可能性があります。必ず @stripe/stripe-js を追加してください。

npm install @stripe/stripe-js

hook が null を返す場合

要素プロバイダの内部にコンポーネントが含まれていないことが原因となります。ツリー構造を再確認し、Providers が適切にレンダリングされているか確認してください。

<!-- 正しい構成 -->
<Elements stripe={promise}>
  <ChildComponent />
</Elements>

<!-- 誤った構成 -->
<ChildComponent />
<Elements ... />

型不一致の解消

オプション引数の型アサーションが必要な場合があります。具体的には定数断言(as const)の使用が有効です。

const config = {
  mode: 'payment' as const,
  currency: 'usd' as const,
  amount: 999,
};

参考資料

より具体的な実装例や最新情報については、以下の公式リソースを参照してください。

  • 公式ドキュメント - React Stripe.js Reference
  • GitHub リポジトリ内のサンプルコード一覧
  • Migration Guide - ビルド手順変更の確認

タグ: React TypeScript Stripe-js frontend-development Payment-Gateway

10月22日 11:05 投稿