TypeScript 実践パターン集 - 型安全性を最大化する高度なテクニック
TypeScriptの高度な型システムを活用した実践的なパターンを詳しく解説。Utility Types、Template Literal Types、Conditional Typesなど、実際のプロジェクトで役立つテクニックを豊富なサンプルコードとともに紹介。
約5分で読めます
技術記事
実践的
この記事のポイント
TypeScriptの高度な型システムを活用した実践的なパターンを詳しく解説。Utility Types、Template Literal Types、Conditional Typesなど、実際のプロジェクトで役立つテクニックを豊富なサンプルコードとともに紹介。
この記事では、実践的なアプローチで技術的な課題を解決する方法を詳しく解説します。具体的なコード例とともに、ベストプラクティスを学ぶことができます。
TypeScriptの真の力は、その高度な型システムにあります。本記事では、実際のプロジェクトで活用できる実践的なTypeScriptパターンを、具体的なユースケースとともに詳しく解説します。初心者から上級者まで、段階的に学べる構成でお届けします。
🎯 型安全なAPI設計パターン
REST APIクライアントの型定義
// 基本的なAPIレスポンス型
interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
message?: string;
timestamp: string;
}
// エンドポイント定義の型安全なパターン
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
interface EndpointConfig<TRequest = void, TResponse = unknown> {
method: HTTPMethod;
path: string;
requestType: TRequest;
responseType: TResponse;
}
// 具体的なAPI定義
type UserAPI = {
getUsers: EndpointConfig<void, ApiResponse<User[]>>;
getUser: EndpointConfig<{ id: number }, ApiResponse<User>>;
createUser: EndpointConfig<CreateUserRequest, ApiResponse<User>>;
updateUser: EndpointConfig<UpdateUserRequest, ApiResponse<User>>;
deleteUser: EndpointConfig<{ id: number }, ApiResponse<void>>;
};
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
createdAt: string;
updatedAt: string;
}
interface CreateUserRequest {
name: string;
email: string;
role?: User['role'];
}
interface UpdateUserRequest extends Partial<CreateUserRequest> {
id: number;
}
型安全なAPIクライアント実装
// APIクライアントの基盤クラス
class TypedApiClient<TEndpoints> {
constructor(private baseUrl: string) {}
async request<K extends keyof TEndpoints>(
endpoint: K,
...args: TEndpoints[K] extends EndpointConfig<infer TReq, any>
? TReq extends void
? []
: [TReq]
: never
): Promise<TEndpoints[K] extends EndpointConfig<any, infer TRes> ? TRes : never> {
// 実装の詳細...
return {} as any;
}
}
// 使用例
const userApi = new TypedApiClient<UserAPI>('https://api.example.com');
// 型安全な呼び出し
const users = await userApi.request('getUsers'); // パラメータ不要
const user = await userApi.request('getUser', { id: 1 }); // id必須
const newUser = await userApi.request('createUser', {
name: 'Alice',
email: 'alice@example.com'
});
// TypeScriptが型エラーを検出
// const invalid = await userApi.request('getUser'); // ❌ パラメータが不足
// const invalid2 = await userApi.request('createUser', { name: 'Alice' }); // ❌ emailが不足
🔧 Utility Typesの実践活用
条件付きプロパティの実装
// 条件に基づいてプロパティを動的に変更
type ConditionalProps<T, K extends keyof T> = T[K] extends boolean
? T[K] extends true
? Required<T>
: Partial<T>
: T;
interface FormConfig {
showAdvanced: boolean;
validateOnBlur: boolean;
autoSave: boolean;
}
interface FormProps<T extends FormConfig> {
config: T;
data: ConditionalProps<
{
username: string;
email: string;
password: string;
confirmPassword: string;
newsletter: boolean;
termsAccepted: boolean;
},
'showAdvanced'
>;
}
// 使用例
const basicForm: FormProps<{ showAdvanced: false; validateOnBlur: true; autoSave: false }> = {
config: { showAdvanced: false, validateOnBlur: true, autoSave: false },
data: {
username: 'john_doe',
email: 'john@example.com',
// password, confirmPassword は省略可能
}
};
const advancedForm: FormProps<{ showAdvanced: true; validateOnBlur: true; autoSave: true }> = {
config: { showAdvanced: true, validateOnBlur: true, autoSave: true },
data: {
username: 'john_doe',
email: 'john@example.com',
password: 'secret123',
confirmPassword: 'secret123',
newsletter: true,
termsAccepted: true, // すべてのプロパティが必須
}
};
深いオブジェクトの型操作
// ネストしたオブジェクトのパス型を生成
type PathsToStringProps<T> = T extends string
? []
: {
[K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
}[Extract<keyof T, string>];
type JoinPaths<T extends ReadonlyArray<string | number>> = T extends readonly [
infer Head,
...infer Tail
]
? Head extends string
? Tail extends ReadonlyArray<string | number>
? Tail['length'] extends 0
? Head
: `${Head}.${JoinPaths<Tail>}`
: never
: never
: never;
// 実践例:設定オブジェクトのパス型生成
interface AppConfig {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
baseUrl: string;
timeout: number;
retries: number;
};
features: {
enableCache: boolean;
enableLogging: boolean;
};
}
type ConfigPaths = JoinPaths<PathsToStringProps<AppConfig>>;
// "database.host" | "database.port" | "database.credentials.username" |
// "database.credentials.password" | "api.baseUrl" | "api.timeout" | ...
// 型安全な設定値取得関数
function getConfigValue<T extends ConfigPaths>(
config: AppConfig,
path: T
): T extends 'database.host' ? string
: T extends 'database.port' ? number
: T extends 'features.enableCache' ? boolean
: unknown {
const keys = path.split('.');
let value: any = config;
for (const key of keys) {
value = value[key];
}
return value;
}
// 使用例
const config: AppConfig = {
database: {
host: 'localhost',
port: 5432,
credentials: { username: 'admin', password: 'secret' }
},
api: { baseUrl: 'https://api.example.com', timeout: 5000, retries: 3 },
features: { enableCache: true, enableLogging: false }
};
const host = getConfigValue(config, 'database.host'); // string型
const port = getConfigValue(config, 'database.port'); // number型
const cacheEnabled = getConfigValue(config, 'features.enableCache'); // boolean型
🎨 Template Literal Typesの活用
動的なイベント型の生成
// イベント名とペイロードの型安全な管理
type EventMap = {
'user.created': { userId: number; email: string };
'user.updated': { userId: number; changes: Partial<User> };
'user.deleted': { userId: number };
'order.placed': { orderId: string; userId: number; total: number };
'order.shipped': { orderId: string; trackingNumber: string };
'order.delivered': { orderId: string; deliveredAt: string };
};
// Template Literal Typesでより柔軟なイベント型を生成
type EntityEvents<T extends string> =
| `${T}.created`
| `${T}.updated`
| `${T}.deleted`
| `${T}.fetched`;
type UserEvents = EntityEvents<'user'>; // 'user.created' | 'user.updated' | 'user.deleted' | 'user.fetched'
type ProductEvents = EntityEvents<'product'>; // 'product.created' | 'product.updated' | ...
// 動的なAPI URL生成
type ApiVersion = 'v1' | 'v2' | 'v3';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ResourcePath<T extends string> = `/${T}` | `/${T}/:id`;
type ApiUrl<V extends ApiVersion, R extends string> = `/api/${V}${ResourcePath<R>}`;
// 使用例
type UserApiUrls = ApiUrl<'v1', 'users'>; // '/api/v1/users' | '/api/v1/users/:id'
type ProductApiUrls = ApiUrl<'v2', 'products'>; // '/api/v2/products' | '/api/v2/products/:id'
// 型安全なルーティング
function createApiRoute<V extends ApiVersion, R extends string>(
version: V,
resource: R,
id?: string
): ApiUrl<V, R> {
const basePath = `/api/${version}/${resource}` as const;
return (id ? `${basePath}/:id` : basePath) as ApiUrl<V, R>;
}
CSS-in-JSの型安全な実装
// CSS プロパティの型定義
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw' | 'vmin' | 'vmax';
type CSSValue<T extends string> = `${number}${T}` | '0';
type Size = CSSValue<CSSUnit> | 'auto' | 'inherit' | 'initial';
// Template Literal Typesで柔軟なCSS型を生成
type ColorValue =
| `#${string}`
| `rgb(${number}, ${number}, ${number})`
| `rgba(${number}, ${number}, ${number}, ${number})`
| `hsl(${number}, ${number}%, ${number}%)`
| 'transparent' | 'currentColor';
interface StyleProps {
width?: Size;
height?: Size;
margin?: Size;
padding?: Size;
backgroundColor?: ColorValue;
color?: ColorValue;
fontSize?: CSSValue<'px' | 'em' | 'rem'>;
borderRadius?: CSSValue<'px' | 'em' | 'rem'>;
}
// 型安全なスタイル関数
function createStyles<T extends Record<string, StyleProps>>(styles: T): T {
return styles;
}
// 使用例
const buttonStyles = createStyles({
primary: {
backgroundColor: '#007bff',
color: '#ffffff',
padding: '12px',
borderRadius: '4px',
fontSize: '16px'
},
secondary: {
backgroundColor: 'transparent',
color: '#007bff',
padding: '12px',
borderRadius: '4px',
fontSize: '16px'
}
});
// TypeScriptが不正な値を検出
// const invalidStyles = createStyles({
// error: {
// backgroundColor: 'invalid-color', // ❌ 型エラー
// padding: '12', // ❌ 単位が不足
// }
// });
🔍 Conditional Typesの実践パターン
関数オーバーロードの代替
// 従来のオーバーロード
function processData(input: string): string;
function processData(input: number): number;
function processData(input: boolean): boolean;
function processData(input: string | number | boolean): string | number | boolean {
return input;
}
// Conditional Typesを使った型安全な実装
type ProcessResult<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: never;
function processDataTyped<T extends string | number | boolean>(input: T): ProcessResult<T> {
return input as ProcessResult<T>;
}
// より複雑な例:データ変換関数
interface TransformMap {
'json': object;
'text': string;
'number': number;
'boolean': boolean;
'date': Date;
}
function transformData<K extends keyof TransformMap>(
data: string,
type: K
): TransformMap[K] {
switch (type) {
case 'json':
return JSON.parse(data) as TransformMap[K];
case 'text':
return data as TransformMap[K];
case 'number':
return Number(data) as TransformMap[K];
case 'boolean':
return Boolean(data) as TransformMap[K];
case 'date':
return new Date(data) as TransformMap[K];
default:
throw new Error(`Unsupported type: ${type}`);
}
}
// 使用例 - 戻り値の型が自動で決まる
const jsonData = transformData('{"name": "Alice"}', 'json'); // object型
const textData = transformData('Hello', 'text'); // string型
const numberData = transformData('42', 'number'); // number型
const dateData = transformData('2024-01-01', 'date'); // Date型
状態管理の型安全性
// Reduxライクな状態管理の型定義
interface BaseAction {
type: string;
payload?: any;
}
type ActionMap<M extends Record<string, any>> = {
[Key in keyof M]: M[Key] extends undefined
? { type: Key }
: { type: Key; payload: M[Key] };
};
// アクション型の定義
type UserActionPayloads = {
'FETCH_USERS_REQUEST': undefined;
'FETCH_USERS_SUCCESS': { users: User[] };
'FETCH_USERS_FAILURE': { error: string };
'CREATE_USER_REQUEST': { userData: CreateUserRequest };
'CREATE_USER_SUCCESS': { user: User };
'CREATE_USER_FAILURE': { error: string };
};
type UserActions = ActionMap<UserActionPayloads>[keyof ActionMap<UserActionPayloads>];
// 状態の型定義
interface UserState {
users: User[];
loading: boolean;
error: string | null;
currentUser: User | null;
}
// 型安全なReducer
function userReducer(state: UserState, action: UserActions): UserState {
switch (action.type) {
case 'FETCH_USERS_REQUEST':
return { ...state, loading: true, error: null };
case 'FETCH_USERS_SUCCESS':
return {
...state,
loading: false,
users: action.payload.users // payload.usersが型安全
};
case 'FETCH_USERS_FAILURE':
return {
...state,
loading: false,
error: action.payload.error // payload.errorが型安全
};
case 'CREATE_USER_SUCCESS':
return {
...state,
users: [...state.users, action.payload.user] // payload.userが型安全
};
default:
return state;
}
}
// アクションクリエーターの型定義
type ActionCreators<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends undefined
? () => { type: K }
: (payload: T[K]) => { type: K; payload: T[K] };
};
// 型安全なアクションクリエーター
const userActions: ActionCreators<UserActionPayloads> = {
'FETCH_USERS_REQUEST': () => ({ type: 'FETCH_USERS_REQUEST' }),
'FETCH_USERS_SUCCESS': (payload) => ({ type: 'FETCH_USERS_SUCCESS', payload }),
'FETCH_USERS_FAILURE': (payload) => ({ type: 'FETCH_USERS_FAILURE', payload }),
'CREATE_USER_REQUEST': (payload) => ({ type: 'CREATE_USER_REQUEST', payload }),
'CREATE_USER_SUCCESS': (payload) => ({ type: 'CREATE_USER_SUCCESS', payload }),
'CREATE_USER_FAILURE': (payload) => ({ type: 'CREATE_USER_FAILURE', payload }),
};
🚀 実践での活用ポイント
型定義の段階的な強化
// 1. 基本的な型定義から開始
interface BasicUser {
id: number;
name: string;
email: string;
}
// 2. ビジネスロジックに応じて拡張
interface EnhancedUser extends BasicUser {
role: 'admin' | 'user' | 'moderator';
permissions: string[];
lastLoginAt: Date | null;
isActive: boolean;
}
// 3. 実行時検証との組み合わせ
import { z } from 'zod';
const UserSchema = z.object({
id: z.number().positive(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'moderator']),
permissions: z.array(z.string()),
lastLoginAt: z.date().nullable(),
isActive: z.boolean()
});
// TypeScriptの型とランタイム検証の統合
type ValidatedUser = z.infer<typeof UserSchema>;
function validateUser(data: unknown): ValidatedUser {
return UserSchema.parse(data);
}
パフォーマンス最適化
// 大きな型の遅延評価
type LazyAPITypes<T> = T extends (...args: any[]) => any
? ReturnType<T>
: T extends Promise<infer U>
? U
: T;
// 型計算の最適化
type OptimizedPick<T, K extends keyof T> = Pick<T, K>;
type OptimizedOmit<T, K extends keyof T> = Omit<T, K>;
// インデックス型の効率的な使用
interface UserLookup {
[id: number]: User;
}
// Map型を活用した型安全なキャッシュ
class TypedCache<T> {
private cache = new Map<string, T>();
set(key: string, value: T): void {
this.cache.set(key, value);
}
get(key: string): T | undefined {
return this.cache.get(key);
}
has(key: string): boolean {
return this.cache.has(key);
}
}
const userCache = new TypedCache<User>();
📚 まとめ
TypeScriptの高度な型システムを活用することで、以下のメリットが得られます:
主要な利点
- 実行時エラーの削減: コンパイル時に多くのエラーを検出
- リファクタリングの安全性: 型システムが変更の影響範囲を明確化
- 開発効率の向上: IDEのサポートによる正確なオートコンプリート
- コードの自己文書化: 型定義がドキュメントとして機能
推奨される導入順序
- 基本的な型アノテーションから開始
- Utility Typesで既存の型を活用
- Conditional Typesで動的な型生成
- Template Literal Typesで文字列ベースの型安全性
これらのパターンを段階的に導入することで、TypeScriptの真の力を活用した、堅牢で保守性の高いアプリケーションを構築できます。公式ドキュメントと併せて学習を進め、実際のプロジェクトで積極的に活用していきましょう。