📝 TypeScript高度な型活用テクニック
Conditional Types、Mapped Types、Template Literal Typesなど、TypeScriptの高度な型システムを実践的に解説
TypeScript高度な型活用テクニック
TypeScriptの型システムは非常に強力で、適切に活用することで実行時エラーを大幅に削減できます。今回は実際の開発で役立つ高度な型のテクニックを紹介します。
Conditional Typesで条件付き型定義
Conditional Typesを使用すると、型の条件に基づいて異なる型を返すことができます。
基本的な使い方
// T が string を継承する場合は number、そうでなければ boolean を返す
type ConditionalType<T> = T extends string ? number : boolean;
type Result1 = ConditionalType<string>; // number
type Result2 = ConditionalType<number>; // boolean
type Result3 = ConditionalType<'hello'>; // number (string literal は string を継承)
実践的な例:API レスポンス型の自動生成
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
}
// APIのレスポンス型を自動生成
type ApiResponse<T> = T extends 'user'
? { data: User; status: 'success' }
: T extends 'post'
? { data: Post; status: 'success' }
: { error: string; status: 'error' };
// 型安全なAPIクライアント
async function fetchData<T extends 'user' | 'post'>(
endpoint: T,
id: number
): Promise<ApiResponse<T>> {
const response = await fetch(`/api/${endpoint}/${id}`);
if (!response.ok) {
return { error: 'Failed to fetch', status: 'error' } as ApiResponse<T>;
}
const data = await response.json();
return { data, status: 'success' } as ApiResponse<T>;
}
// 使用例:型推論が効く
const userResponse = await fetchData('user', 1);
// userResponse.data は User 型として推論される
const postResponse = await fetchData('post', 1);
// postResponse.data は Post 型として推論される
Mapped Typesで既存型の変換
Mapped Typesを使用すると、既存の型のプロパティを変換して新しい型を作成できます。
基本的なMapped Types
interface User {
id: number;
name: string;
email: string;
age: number;
}
// すべてのプロパティをオプショナルにする
type PartialUser = {
[K in keyof User]?: User[K];
};
// すべてのプロパティを読み取り専用にする
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// 特定のプロパティのみを選択
type UserSummary = {
[K in 'id' | 'name']: User[K];
};
実践的な例:フォームバリデーション
// バリデーションルール型
type ValidationRule<T> = {
required?: boolean;
minLength?: T extends string ? number : never;
maxLength?: T extends string ? number : never;
min?: T extends number ? number : never;
max?: T extends number ? number : never;
pattern?: T extends string ? RegExp : never;
};
// フォーム型からバリデーション設定型を自動生成
type FormValidation<T> = {
[K in keyof T]?: ValidationRule<T[K]>;
};
interface RegistrationForm {
username: string;
email: string;
age: number;
password: string;
}
// 型安全なバリデーション設定
const validationRules: FormValidation<RegistrationForm> = {
username: {
required: true,
minLength: 3,
maxLength: 20,
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
age: {
required: true,
min: 18,
max: 120,
},
password: {
required: true,
minLength: 8,
},
};
// バリデーション関数
function validateForm<T>(
data: T,
rules: FormValidation<T>
): { isValid: boolean; errors: Partial<Record<keyof T, string>> } {
const errors: Partial<Record<keyof T, string>> = {};
for (const key in rules) {
const value = data[key];
const rule = rules[key];
if (!rule) continue;
if (rule.required && !value) {
errors[key] = `${String(key)} is required`;
continue;
}
if (typeof value === 'string') {
if (rule.minLength && value.length < rule.minLength) {
errors[key] = `${String(key)} must be at least ${rule.minLength} characters`;
}
if (rule.maxLength && value.length > rule.maxLength) {
errors[key] = `${String(key)} must be at most ${rule.maxLength} characters`;
}
if (rule.pattern && !rule.pattern.test(value)) {
errors[key] = `${String(key)} format is invalid`;
}
}
if (typeof value === 'number') {
if (rule.min && value < rule.min) {
errors[key] = `${String(key)} must be at least ${rule.min}`;
}
if (rule.max && value > rule.max) {
errors[key] = `${String(key)} must be at most ${rule.max}`;
}
}
}
return {
isValid: Object.keys(errors).length === 0,
errors,
};
}
Template Literal Typesで動的な型生成
Template Literal Typesを使用すると、文字列リテラル型を組み合わせて新しい型を作成できます。
CSS-in-JSの型安全性
// カラーパレット
type Color = 'red' | 'blue' | 'green' | 'yellow';
type Shade = '100' | '200' | '300' | '400' | '500';
// 動的にカラークラス名を生成
type ColorClass = `text-${Color}-${Shade}` | `bg-${Color}-${Shade}`;
// 使用例
const validClasses: ColorClass[] = [
'text-red-100',
'bg-blue-500',
'text-green-300',
// 'text-purple-100', // エラー: purple は Color に含まれない
];
// より複雑な例:レスポンシブクラス
type Breakpoint = 'sm' | 'md' | 'lg' | 'xl';
type ResponsiveColorClass = ColorClass | `${Breakpoint}:${ColorClass}`;
const responsiveClasses: ResponsiveColorClass[] = [
'text-red-100',
'md:bg-blue-500',
'lg:text-green-300',
];
APIエンドポイントの型安全性
// RESTful APIエンドポイント型
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'users' | 'posts' | 'comments';
type ApiEndpoint<M extends HttpMethod, R extends Resource> =
M extends 'GET'
? `/api/${R}` | `/api/${R}/${number}`
: M extends 'POST'
? `/api/${R}`
: M extends 'PUT' | 'DELETE'
? `/api/${R}/${number}`
: never;
// 型安全なAPIクライアント
async function apiRequest<M extends HttpMethod, R extends Resource>(
method: M,
endpoint: ApiEndpoint<M, R>,
data?: any
) {
const response = await fetch(endpoint, {
method,
headers: {
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
});
return response.json();
}
// 使用例
apiRequest('GET', '/api/users'); // ✅ 有効
apiRequest('GET', '/api/users/123'); // ✅ 有効
apiRequest('POST', '/api/posts'); // ✅ 有効
apiRequest('PUT', '/api/comments/456'); // ✅ 有効
// apiRequest('POST', '/api/users/123'); // ❌ エラー: POSTは個別リソースに対して無効
Utility Typesの組み合わせ技
TypeScriptの組み込みUtility Typesを組み合わせることで、複雑な型変換を簡潔に表現できます。
深い部分オプショナル化
// ネストした型の部分的オプショナル化
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
baseUrl: string;
timeout: number;
};
}
// 部分的な設定の上書きが可能
function updateConfig(updates: DeepPartial<Config>): Config {
// 既存の設定とマージするロジック
return mergeDeep(defaultConfig, updates);
}
// 使用例
updateConfig({
database: {
port: 5432, // host や credentials は省略可能
},
});
型安全なイベントシステム
// イベント型定義
interface EventMap {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'post:created': { postId: string; authorId: string; title: string };
'post:updated': { postId: string; changes: string[] };
}
type EventName = keyof EventMap;
// 型安全なイベントエミッター
class TypedEventEmitter {
private listeners: {
[K in EventName]?: Array<(data: EventMap[K]) => void>;
} = {};
on<K extends EventName>(
event: K,
listener: (data: EventMap[K]) => void
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends EventName>(event: K, data: EventMap[K]): void {
const eventListeners = this.listeners[event];
if (eventListeners) {
eventListeners.forEach(listener => listener(data));
}
}
off<K extends EventName>(
event: K,
listener: (data: EventMap[K]) => void
): void {
const eventListeners = this.listeners[event];
if (eventListeners) {
this.listeners[event] = eventListeners.filter(l => l !== listener);
}
}
}
// 使用例
const emitter = new TypedEventEmitter();
emitter.on('user:login', (data) => {
// data は { userId: string; timestamp: Date } として型推論される
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
emitter.emit('user:login', {
userId: '123',
timestamp: new Date(),
});
// emitter.emit('user:login', { userId: 123 }); // エラー: userId は string である必要がある
まとめ
TypeScriptの高度な型システムを活用することで:
- 実行時エラーの削減: コンパイル時により多くのエラーを検出
- 開発効率の向上: IDEの補完機能がより正確に
- 保守性の向上: 型が仕様書の役割を果たす
- リファクタリングの安全性: 型システムが変更の影響範囲を明確化
これらのテクニックを組み合わせることで、より堅牢で保守性の高いTypeScriptアプリケーションを構築できます。型システムの力を最大限に活用して、品質の高いコードを書いていきましょう。