TypeScript declare の実務活用ガイド|型定義で外部ライブラリを安全に扱う
はじめに
TypeScript を使う開発現場で、しばしば「型定義がない外部ライブラリ」や「実行時に存在するグローバルオブジェクト」と遭遇します。そんなとき活躍するのが declare キーワードです。本記事では、declare の実務的な使い方を、実際の業務シーンを想定したコード例を交えて解説します。
declare キーワードの基礎
declare はTypeScript特有のキーワードで、「このコードは実行時に存在するが、TypeScriptコンパイル時には定義がない」という状況を型チェッカーに伝えるためのものです。JavaScript のコードは実行されず、型定義のみがTypeScriptに認識されます。
基本的な使い方は以下の通りです:
// グローバル変数を declare する
declare const API_KEY: string;
// 関数を declare する
declare function initializeSDK(config: object): void;
// クラスを declare する
declare class PaymentGateway {
charge(amount: number): Promise<string>;
}
// モジュール全体を declare する
declare module 'analytics-library' {
export function trackEvent(eventName: string, data: object): void;
}
これらは実装を持たず、型情報のみをTypeScriptに提供します。
業務でのユースケース
1. サードパーティSDKの型定義が不完全または存在しない場合
決済ゲートウェイやアナリティクスツールなど、型定義ファイル(.d.ts)が提供されていないライブラリは珍しくありません。そのような場合に declare を使用します。
2. Window オブジェクトへの動的なプロパティ追加
グローバルスコープに実行時に追加されるプロパティがある場合、TypeScriptには事前に知らせる必要があります。
3. レガシーコードベースとの統合
既存のJavaScriptコードと TypeScript を段階的に統合する際、外部スクリプトで定義されたグローバル変数を安全に扱う必要があります。
実装コード例
実務例1:決済ゲートウェイの型定義
実際のプロジェクトでよく見かけるシナリオです。Stripe や Braintree などの決済SDKは、グローバルスコープに自身を登録します。
// types/payment.d.ts
declare global {
interface Window {
Stripe: StripeConstructor;
}
}
interface StripeConstructor {
(publicKey: string): StripeInstance;
}
interface StripeInstance {
createPaymentMethod(options: PaymentMethodOptions): Promise<PaymentMethodResult>;
confirmCardPayment(clientSecret: string, data: PaymentData): Promise<ConfirmResult>;
}
interface PaymentMethodOptions {
type: 'card';
card: { token: string };
billing_details?: BillingDetails;
}
interface BillingDetails {
name?: string;
email?: string;
phone?: string;
address?: AddressInfo;
}
interface AddressInfo {
postal_code: string;
country: string;
}
interface PaymentMethodResult {
paymentMethod?: { id: string };
error?: { message: string };
}
interface PaymentData {
payment_method: string;
metadata?: Record<string, any>;
}
interface ConfirmResult {
paymentIntent?: { status: string; id: string };
error?: { message: string };
}
export {};
このファイルをプロジェクトに置くと、TypeScript はこれらの型定義を認識します。
// src/services/paymentService.ts
import type { StripeInstance } from '../types/payment';
export class PaymentService {
private stripe: StripeInstance;
constructor(publicKey: string) {
// Stripe スクリプトはHTMLで読み込み済みと想定
this.stripe = window.Stripe(publicKey);
}
async processPayment(token: string, amount: number) {
try {
const paymentMethod = await this.stripe.createPaymentMethod({
type: 'card',
card: { token },
billing_details: {
name: 'John Doe',
email: 'john@example.com',
address: {
postal_code: '12345',
country: 'US'
}
}
});
if (paymentMethod.error) {
throw new Error(paymentMethod.error.message);
}
return paymentMethod.paymentMethod?.id;
} catch (error) {
console.error('Payment processing failed:', error);
throw error;
}
}
async confirmPayment(clientSecret: string, paymentMethodId: string) {
const result = await this.stripe.confirmCardPayment(clientSecret, {
payment_method: paymentMethodId,
metadata: {
timestamp: new Date().toISOString()
}
});
if (result.error) {
throw new Error(result.error.message);
}
return result.paymentIntent;
}
}
実務例2:アナリティクスライブラリの統合
多くのアナリティクスツールはグローバルキューを使用するパターンです。
// types/analytics.d.ts
declare global {
interface Window {
gtag: (command: string, action: string, params?: Record<string, any>) => void;
dataLayer?: Record<string, any>[];
}
}
declare const gtag: (command: string, action: string, params?: Record<string, any>) => void;
export {};
// src/services/analyticsService.ts
export class AnalyticsService {
/**
* ページビューを記録
*/
static pageView(pagePath: string, pageTitle: string) {
if (typeof window !== 'undefined' && window.gtag) {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: pagePath,
page_title: pageTitle,
timestamp: new Date().getTime()
});
}
}
/**
* カスタムイベントを記録(購入完了など)
*/
static trackPurchase(
transactionId: string,
value: number,
currency: string,
items: Array<{ id: string; name: string; price: number }>
) {
if (typeof window !== 'undefined' && window.gtag) {
gtag('event', 'purchase', {
transaction_id: transactionId,
value,
currency,
items: items.map(item => ({
item_id: item.id,
item_name: item.name,
price: item.price
}))
});
}
}
/**
* ユーザーIDを設定
*/
static setUserId(userId: string) {
if (typeof window !== 'undefined' && window.gtag) {
gtag('config', 'GA_MEASUREMENT_ID', {
user_id: userId
});
}
}
/**
* カスタム属性を設定
*/
static setUserProperties(properties: Record<string, string | number>) {
if (typeof window !== 'undefined' && window.gtag) {
gtag('set', 'user_properties', properties);
}
}
}
実務例3:複雑なモジュール型定義
複数の機能を持つライブラリの場合、モジュール全体を declare することもあります。
// types/custom-logger.d.ts
declare module 'custom-logger' {
interface LogContext {
userId?: string;
sessionId?: string;
environment?: 'development' | 'production' | 'staging';
[key: string]: any;
}
interface LoggerConfig {
level: 'debug' | 'info' | 'warn' | 'error';
format: 'json' | 'text';
transporters: Array<{
type: 'console' | 'file' | 'remote';
url?: string;
path?: string;
}>;
}
class Logger {
constructor(config: LoggerConfig);
setContext(context: LogContext): void;
debug(message: string, data?: any): void;
info(message: string, data?: any): void;
warn(message: string, data?: any): void;
error(message: string, error?: Error | string, data?: any): void;
flush(): Promise<void>;
}
export = Logger;
}
// src/utils/logger.ts
import Logger from 'custom-logger';
export const createLogger = (environment: string) => {
const logger = new Logger({
level: environment === 'production' ? 'warn' : 'debug',
format: 'json',
transporters: [
{
type: 'console'
},
...(environment === 'production'
? [
{
type: 'remote',
url: 'https://logs.example.com/api/logs'
}
]
: [])
]
});
logger.setContext({
environment: environment as any,
timestamp: new Date().toISOString()
});
return logger;
};
// 使用例
const logger = createLogger(process.env.NODE_ENV || 'development');
export const logUserAction = async (userId: string, action: string, details?: any) => {
logger.setContext({
userId,
sessionId: sessionStorage.getItem('sessionId') || undefined
});
logger.info(`User action: ${action}`, {
action,
timestamp: new Date().toISOString(),
...details
});
// 本番環境ではログをリモートに送信
if (process.env.NODE_ENV === 'production') {
await logger.flush();
}
};
よくある応用パターン
パターン1:条件付き型定義
環境に応じて異なるグローバル変数を定義する必要がある場合があります。
// types/environment.d.ts
declare global {
interface Window {
__DEV__: boolean;
__API_BASE_URL__: string;
}
namespace NodeJS {
interface ProcessEnv {
REACT_APP_API_BASE: string;
REACT_APP_STRIPE_KEY: string;
REACT_APP_ANALYTICS_ID: string;
}
}
}
// ビルド時に注入される値
declare const __VERSION__: string;
declare const __BUILD_TIME__: string;
export {};
// src/config.ts
export const getConfig = () => {
return {
isDevelopment: window.__DEV__,
apiBaseUrl: window.__API_BASE_URL__,
version: __VERSION__,
buildTime: __BUILD_TIME__,
stripeKey: process.env.REACT_APP_STRIPE_KEY,
analyticsId: process.env.REACT_APP_ANALYTICS_ID
};
};
パターン2:名前空間を使った整理
複数の関連する型定義をグループ化する場合、名前空間が便利です。
// types/thirdparty.d.ts\ndeclare global {\n namespace ThirdPartyAPIs {\n interface FacebookSDK {\n init(config: FacebookConfig): void;\n track(event: string, data: object): void;\n }\n\n interface LinkedInInsight {\n track(conversions: ConversionData): void;\n }\n\n interface FacebookConfig {\n appId: string;\n version: string;\n }\n\n interface ConversionData {\n conversionId: string;\n value: number;\n currency: string;\n }\n }\n\n interface Window {\n fbq: ThirdPartyAPIs.FacebookSDK;\n lintrk: ThirdPartyAPIs.LinkedInInsight;\n }\n}\n\nexport {};
パターン3:型安全なイベントエミッター
グローバルイベントを型安全に扱う実務的なパターンです。
// types/events.d.ts\ndeclare global {\n interface GlobalEventMap {\n 'user:login': { userId: string; timestamp: Date };\n 'user:logout': { userId: string };\n 'cart:updated': { itemCount: number; totalPrice: number };\n 'error:occurred': { message: string; code: string; context?: object };\n }\n\n interface Window {\n __eventBus: EventBus;\n }\n}\n\ninterface EventBus {\n on<K extends keyof GlobalEventMap>(\n event: K,\n callback: (data: GlobalEventMap[K]) => void\n ): void;\n emit<K extends keyof GlobalEventMap>(\n event: K,\n data: GlobalEventMap[K]\n ): void;\n}\n\nexport {};
// src/eventBus.ts\nclass TypeSafeEventBus implements EventBus {\n private listeners: Map<string, Set<Function>> = new Map();\n\n on<K extends keyof GlobalEventMap>(\n event: K,\n callback: (data: GlobalEventMap[K]) => void\n ) {\n if (!this.listeners.has(event as string)) {\n this.listeners.set(event as string, new Set());\n }\n this.listeners.get(event as string)!.add(callback);\n }\n\n emit<K extends keyof GlobalEventMap>(\n event: K,\n data: GlobalEventMap[K]\n ) {\n const callbacks = this.listeners.get(event as string);\n if (callbacks) {\n callbacks.forEach(callback => callback(data));\n }\n }\n}\n\nif (typeof window !== 'undefined') {\n window.__eventBus = new TypeSafeEventBus();\n}\n\n// 使用例\nwindow.__eventBus.on('user:login', (data) => {\n // data は { userId: string; timestamp: Date } として型推論される\n console.log(`User ${data.userId} logged in at ${data.timestamp}`);\n});\n\nwindow.__eventBus.emit('user:login', {\n userId: '12345',\n timestamp: new Date()\n});
注意点とベストプラクティス
1. declare は実装ではなく型宣言のみ
よくある間違いは、declare 内に実装を書こうとすることです。declare はコンパイル時にしか存在しないため、実装は別に必要です。
// ❌ 間違い:実装を含む
declare function myFunction(x: number) {
return x * 2;
}\n\n// ✅ 正しい:型定義のみ\ndeclare function myFunction(x: number): number;
2. declare global の使い方に注意
declare global を複数のファイルで使う場合、各ファイルの最後に export {} を記載してモジュールとして認識させましょう。
// types/globals.d.ts\ndeclare global {\n interface Window {\n myGlobal: string;\n }\n}\n\n// このエクスポートがないと、TypeScript がモジュールと認識しない場合がある\nexport {};
3. 実行時の存在確認
declare はTypeScriptにのみ伝えるもので、実行時に実際に存在するかは保証されません。必ずガード句を使いましょう。
// ❌ 危険:declare したから必ず存在すると仮定\nwindow.myFunction();\n\n// ✅ 安全:実行時に確認\nif (typeof window !== 'undefined' && window.myFunction) {\n window.myFunction();\n}
4. 型定義ファイルの場所
プロジェクトルートに types フォルダを作成し、declare はそこに集約するのが慣例です。tsconfig.json で参照パスを設定します。
// tsconfig.json\n{\n \"compilerOptions\": {\n \"typeRoots\": [\"./node_modules/@types\", \"./types\"],\n \"baseUrl\": \".\",\n \"paths\": {\n \"@types/*\": [\"types/*\"]\n }\n },\n \"include\": [\"src/**/*\", \"types/**/*.d.ts\"]\n}
5. @types パッケージの優先
サードパーティライブラリの型定義がある場合、npm の @types スコープのパッケージが存在しないか確認してください。わざわざ declare で定義するより、既存の型定義を使う方が保守性が高いです。
npm search @types/lodash
npm install --save-dev @types/lodash
6. Any の回避
declare は型定義を簡略化するために使いがちですが、 any を多用すると TypeScript の利点が失われます。可能な限り具体的な型を定義しましょう。
// ❌ 避けるべき\ndeclare const api: any;\n\n// ✅ 具体的に定義\ndeclare const api: {\n get(url: string): Promise<Response>;\n post(url: string, data: object): Promise<Response>;\n};
まとめ
TypeScript の declare キーワードは、型定義がない外部ライブラリやグローバルオブジェクトを安全に扱うための重要な機能です。実務では以下の3つのシーンで頻出します。
- サードパーティSDK:決済ゲートウェイやアナリティクスツールの型定義
- グローバルスコープの拡張:Window オブジェクトへの属性追加
- レガシーコード統合:既存JavaScriptとTypeScriptの共存
実装時の重要ポイントは:
- declare は型宣言のみ、実装ではない
- 必ず実行時に存在確認を行う
- 型定義は可能な限り具体的に
- declare global は
export {}でモジュール化 - 既存の @types パッケージがないか先に確認
正しく使うことで、型安全性を保ちながら、JavaScriptの柔軟性を活かした開発ができます。業務コードでの declare 活用は、チーム全体の開発効率と保守性を大きく向上させるでしょう。

