TypeScript Function Overloads を使った柔軟な API 設計パターン

TypeScript で API を設計する際に「引数のパターンによって戻り値の型を変えたい」「同じ関数名で異なる使い方をサポートしたい」といった要求に直面したことはありませんか?従来の JavaScript では実現が困難だったこれらの要求を、TypeScript の Function Overloads を使うことで型安全かつ直感的に解決できます。
本記事では、Function Overloads を活用した柔軟な API 設計パターンを詳しく解説します。ライブラリ開発者にとって必須のテクニックから、実際のプロジェクトで即座に活用できる実践的なパターンまで、あなたの TypeScript スキルを次のレベルに押し上げる知識をお届けします。
基本概念と設計思想
TypeScript の Function Overloads を効果的に活用するためには、まずその理論的背景と実装メカニズムを理解することが重要です。
Function Overloads の基本メカニズム
Function Overloads は、同じ関数名で異なるシグネチャを定義し、引数の型や数に応じて適切な戻り値の型を提供する仕組みです。
基本的な Overload の定義
typescript// 基本的な Function Overloads の例
function processData(data: string): string;
function processData(data: number): number;
function processData(data: boolean): boolean;
function processData(
data: string | number | boolean
): string | number | boolean {
if (typeof data === 'string') {
return data.toUpperCase();
} else if (typeof data === 'number') {
return data * 2;
} else {
return !data;
}
}
// 使用例:TypeScript が適切な戻り値の型を推論
const stringResult = processData('hello'); // string 型
const numberResult = processData(42); // number 型
const booleanResult = processData(true); // boolean 型
Overload シグネチャと実装シグネチャの関係
typescript// Overload シグネチャ(型定義のみ)
function createResource(id: string): Promise<Resource>;
function createResource(
config: ResourceConfig
): Promise<Resource>;
// 実装シグネチャ(実際の処理)
function createResource(
input: string | ResourceConfig
): Promise<Resource> {
if (typeof input === 'string') {
// ID から Resource を作成
return fetch(`/api/resources/${input}`).then(
(response) => response.json()
);
} else {
// 設定オブジェクトから Resource を作成
return fetch('/api/resources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
}).then((response) => response.json());
}
}
interface Resource {
id: string;
name: string;
type: string;
}
interface ResourceConfig {
name: string;
type: string;
metadata?: Record<string, unknown>;
}
引数パターンによる型推論の最適化
Function Overloads の真の価値は、引数の組み合わせに応じて最適な型推論を実現することにあります。
引数の有無による戻り値型の変化
typescript// 引数パターンによる型の最適化
interface QueryOptions {
limit?: number;
offset?: number;
sort?: string;
}
interface User {
id: string;
name: string;
email: string;
}
interface UserWithMetadata extends User {
createdAt: Date;
updatedAt: Date;
loginCount: number;
}
// Overload による戻り値型の最適化
function getUsers(): Promise<User[]>;
function getUsers(options: QueryOptions): Promise<User[]>;
function getUsers(id: string): Promise<User | null>;
function getUsers(
id: string,
includeMetadata: true
): Promise<UserWithMetadata | null>;
function getUsers(
idOrOptions?: string | QueryOptions,
includeMetadata?: boolean
): Promise<User[] | User | UserWithMetadata | null> {
if (typeof idOrOptions === 'string') {
// 単一ユーザーの取得
const userId = idOrOptions;
if (includeMetadata) {
return fetch(
`/api/users/${userId}?include=metadata`
).then((response) =>
response.json()
) as Promise<UserWithMetadata>;
} else {
return fetch(`/api/users/${userId}`).then(
(response) => response.json()
) as Promise<User>;
}
} else {
// ユーザーリストの取得
const queryParams = new URLSearchParams();
if (idOrOptions?.limit)
queryParams.set('limit', String(idOrOptions.limit));
if (idOrOptions?.offset)
queryParams.set('offset', String(idOrOptions.offset));
if (idOrOptions?.sort)
queryParams.set('sort', idOrOptions.sort);
return fetch(`/api/users?${queryParams}`).then(
(response) => response.json()
) as Promise<User[]>;
}
}
// 使用例:コンパイル時に適切な型が推論される
async function example() {
const allUsers = await getUsers(); // Promise<User[]>
const limitedUsers = await getUsers({ limit: 10 }); // Promise<User[]>
const singleUser = await getUsers('user123'); // Promise<User | null>
const userWithMeta = await getUsers('user123', true); // Promise<UserWithMetadata | null>
}
条件付き型との組み合わせ
typescript// より高度な型推論のパターン
type ApiResponse<
T extends 'single' | 'list',
U
> = T extends 'single' ? U | null : U[];
interface FetchOptions<T extends 'single' | 'list'> {
type: T;
params?: Record<string, string | number>;
}
// 条件付き型を活用した Overload
function fetchApi<T>(
endpoint: string,
options: FetchOptions<'single'>
): Promise<ApiResponse<'single', T>>;
function fetchApi<T>(
endpoint: string,
options: FetchOptions<'list'>
): Promise<ApiResponse<'list', T>>;
function fetchApi<T>(
endpoint: string,
options: FetchOptions<'single' | 'list'>
): Promise<T | T[] | null> {
const { type, params } = options;
const queryString = params
? '?' +
new URLSearchParams(
params as Record<string, string>
).toString()
: '';
return fetch(`${endpoint}${queryString}`)
.then((response) => response.json())
.then((data) => {
if (type === 'single') {
return data?.items?.[0] || null;
} else {
return data?.items || [];
}
});
}
// 使用例
async function advancedExample() {
// 単一のアイテムを取得(T | null 型)
const singleProduct = await fetchApi<Product>(
'/products',
{
type: 'single',
params: { id: '123' },
}
);
// 複数のアイテムを取得(T[] 型)
const productList = await fetchApi<Product>('/products', {
type: 'list',
params: { category: 'electronics' },
});
}
コンパイラによる最適なシグネチャ選択
TypeScript コンパイラは、Overload の定義順序と引数の適合性を評価して、最適なシグネチャを選択します。
シグネチャの優先順位制御
typescript// シグネチャの順序が重要な例
interface ProcessingOptions {
async?: boolean;
timeout?: number;
}
// ❌ 問題のある Overload 順序
function badProcess(data: unknown): string;
function badProcess(
data: string,
options: ProcessingOptions
): string;
function badProcess(data: string): string; // 到達不可能
function badProcess(
data: unknown,
options?: ProcessingOptions
): string {
// 実装
return String(data);
}
// ✅ 適切な Overload 順序
function goodProcess(
data: string,
options: ProcessingOptions
): string;
function goodProcess(data: string): string;
function goodProcess(data: unknown): string;
function goodProcess(
data: unknown,
options?: ProcessingOptions
): string {
if (typeof data === 'string' && options) {
// 最も具体的なケース
return options.async ? `async:${data}` : `sync:${data}`;
} else if (typeof data === 'string') {
// 中程度の具体性
return `default:${data}`;
} else {
// 最も汎用的なケース
return `generic:${String(data)}`;
}
}
型ガードとの効果的な組み合わせ
typescript// 型ガードを活用した精密な Overload
interface DatabaseRecord {
id: string;
type: string;
}
interface UserRecord extends DatabaseRecord {
type: 'user';
name: string;
email: string;
}
interface ProductRecord extends DatabaseRecord {
type: 'product';
name: string;
price: number;
}
type AnyRecord = UserRecord | ProductRecord;
// 型ガード関数
function isUserRecord(
record: AnyRecord
): record is UserRecord {
return record.type === 'user';
}
function isProductRecord(
record: AnyRecord
): record is ProductRecord {
return record.type === 'product';
}
// 型ガードを活用した Overload
function processRecord(record: UserRecord): string;
function processRecord(record: ProductRecord): number;
function processRecord(record: AnyRecord): string | number {
if (isUserRecord(record)) {
return `User: ${record.name} (${record.email})`;
} else if (isProductRecord(record)) {
return record.price;
} else {
throw new Error('Unknown record type');
}
}
// 使用例
const userRecord: UserRecord = {
id: '1',
type: 'user',
name: 'John Doe',
email: 'john@example.com',
};
const productRecord: ProductRecord = {
id: '2',
type: 'product',
name: 'Laptop',
price: 1000,
};
const userResult = processRecord(userRecord); // string 型
const productResult = processRecord(productRecord); // number 型
実用的な Overload パターン集
実際のプロジェクトで頻繁に使用される、具体的な Overload パターンを詳しく解説します。
条件分岐型 Overload(引数の有無による型変化)
引数の有無や組み合わせによって、戻り値の型や処理内容を変更するパターンです。
設定オブジェクトの有無による動作変更
typescript// 設定の有無による動作分岐
interface LoggerConfig {
level: 'debug' | 'info' | 'warn' | 'error';
format: 'json' | 'text';
timestamp: boolean;
}
interface LogEntry {
message: string;
level: string;
timestamp?: Date;
formatted: string;
}
// 引数パターンによる戻り値型の変化
function createLogger(): (message: string) => void;
function createLogger(
config: LoggerConfig
): (message: string) => LogEntry;
function createLogger(config?: LoggerConfig) {
if (config) {
// 設定ありの場合:詳細なログエントリを返す
return (message: string): LogEntry => {
const entry: LogEntry = {
message,
level: config.level,
formatted:
config.format === 'json'
? JSON.stringify({
message,
level: config.level,
})
: `[${config.level.toUpperCase()}] ${message}`,
};
if (config.timestamp) {
entry.timestamp = new Date();
}
console.log(entry.formatted);
return entry;
};
} else {
// 設定なしの場合:シンプルなログ出力
return (message: string): void => {
console.log(message);
};
}
}
// 使用例
const simpleLogger = createLogger(); // (message: string) => void
const advancedLogger = createLogger({
// (message: string) => LogEntry
level: 'info',
format: 'json',
timestamp: true,
});
simpleLogger('Hello World'); // void
const logEntry = advancedLogger('Debug info'); // LogEntry
オプション引数による結果の型変更
typescript// オプション引数による型の細分化
interface SearchOptions {
includeMetadata?: boolean;
includeTags?: boolean;
includeRelations?: boolean;
}
interface BaseItem {
id: string;
title: string;
content: string;
}
interface ItemWithMetadata extends BaseItem {
createdAt: Date;
updatedAt: Date;
author: string;
}
interface ItemWithTags extends BaseItem {
tags: string[];
}
interface ItemWithRelations extends BaseItem {
relatedItems: string[];
}
type EnrichedItem = BaseItem &
Partial<ItemWithMetadata> &
Partial<ItemWithTags> &
Partial<ItemWithRelations>;
// オプションに応じた型推論
function searchItems(query: string): Promise<BaseItem[]>;
function searchItems(
query: string,
options: { includeMetadata: true }
): Promise<ItemWithMetadata[]>;
function searchItems(
query: string,
options: { includeTags: true }
): Promise<ItemWithTags[]>;
function searchItems(
query: string,
options: { includeRelations: true }
): Promise<ItemWithRelations[]>;
function searchItems(
query: string,
options: SearchOptions
): Promise<EnrichedItem[]>;
function searchItems(
query: string,
options?: SearchOptions
): Promise<
| BaseItem[]
| ItemWithMetadata[]
| ItemWithTags[]
| ItemWithRelations[]
| EnrichedItem[]
> {
const baseUrl = `/api/search?q=${encodeURIComponent(
query
)}`;
const params = new URLSearchParams();
if (options?.includeMetadata)
params.append('include', 'metadata');
if (options?.includeTags)
params.append('include', 'tags');
if (options?.includeRelations)
params.append('include', 'relations');
const url = params.toString()
? `${baseUrl}&${params}`
: baseUrl;
return fetch(url).then((response) => response.json());
}
// 使用例
async function searchExample() {
const basicResults = await searchItems('typescript'); // BaseItem[]
const withMeta = await searchItems('typescript', {
includeMetadata: true,
}); // ItemWithMetadata[]
const withTags = await searchItems('typescript', {
includeTags: true,
}); // ItemWithTags[]
const enriched = await searchItems('typescript', {
includeMetadata: true,
includeTags: true,
}); // EnrichedItem[]
}
ジェネリック活用型 Overload(型パラメータとの組み合わせ)
ジェネリクスと Overload を組み合わせることで、より柔軟で型安全な API を設計できます。
型パラメータによる戻り値型の動的変更
typescript// ジェネリクスと Overload の組み合わせ
interface Serializer<T> {
serialize(data: T): string;
deserialize(data: string): T;
}
interface JsonSerializer extends Serializer<unknown> {
type: 'json';
}
interface XmlSerializer extends Serializer<object> {
type: 'xml';
}
interface BinarySerializer extends Serializer<ArrayBuffer> {
type: 'binary';
}
// ジェネリック Overload の定義
function createSerializer<T>(type: 'json'): JsonSerializer;
function createSerializer<T extends object>(
type: 'xml'
): XmlSerializer;
function createSerializer<T extends ArrayBuffer>(
type: 'binary'
): BinarySerializer;
function createSerializer<T>(
type: 'json' | 'xml' | 'binary'
): JsonSerializer | XmlSerializer | BinarySerializer {
switch (type) {
case 'json':
return {
type: 'json',
serialize: (data: unknown) => JSON.stringify(data),
deserialize: (data: string) => JSON.parse(data),
} as JsonSerializer;
case 'xml':
return {
type: 'xml',
serialize: (data: object) => {
// XML シリアライゼーション実装
return `<root>${JSON.stringify(data)}</root>`;
},
deserialize: (data: string) => {
// XML デシリアライゼーション実装
return JSON.parse(data.replace(/<\/?root>/g, ''));
},
} as XmlSerializer;
case 'binary':
return {
type: 'binary',
serialize: (data: ArrayBuffer) => {
return Array.from(new Uint8Array(data))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
},
deserialize: (data: string) => {
const bytes =
data
.match(/.{2}/g)
?.map((byte) => parseInt(byte, 16)) || [];
return new Uint8Array(bytes).buffer;
},
} as BinarySerializer;
default:
throw new Error(
`Unsupported serializer type: ${type}`
);
}
}
// 使用例
const jsonSerializer = createSerializer('json'); // JsonSerializer
const xmlSerializer = createSerializer('xml'); // XmlSerializer
const binarySerializer = createSerializer('binary'); // BinarySerializer
制約付きジェネリクスでの型安全性確保
typescript// 制約付きジェネリクスを活用した高度な Overload
interface Validatable {
validate(): boolean;
}
interface Persistable {
save(): Promise<void>;
}
interface Entity extends Validatable, Persistable {
id: string;
}
interface User extends Entity {
name: string;
email: string;
}
interface Product extends Entity {
name: string;
price: number;
}
// 制約付きジェネリクス Overload
function processEntity<T extends User>(
entity: T,
action: 'user-specific'
): Promise<T & { processed: true }>;
function processEntity<T extends Product>(
entity: T,
action: 'product-specific'
): Promise<T & { calculated: number }>;
function processEntity<T extends Entity>(
entity: T,
action: 'generic'
): Promise<T & { validated: boolean }>;
function processEntity<T extends Entity>(
entity: T,
action: 'user-specific' | 'product-specific' | 'generic'
): Promise<
T &
(
| { processed: true }
| { calculated: number }
| { validated: boolean }
)
> {
const isValid = entity.validate();
switch (action) {
case 'user-specific':
if ('email' in entity) {
return entity.save().then(
() =>
({
...entity,
processed: true,
} as T & { processed: true })
);
}
break;
case 'product-specific':
if ('price' in entity) {
return entity.save().then(
() =>
({
...entity,
calculated: (entity as Product).price * 1.1,
} as T & { calculated: number })
);
}
break;
case 'generic':
return entity.save().then(
() =>
({
...entity,
validated: isValid,
} as T & { validated: boolean })
);
}
throw new Error('Invalid entity or action combination');
}
Union 型活用 Overload(複数型の柔軟な受け入れ)
Union 型と Overload を組み合わせることで、多様な入力を型安全に処理できます。
入力型に応じた処理分岐
typescript// Union 型を活用した柔軟な入力処理
type InputData = string | number | object | Array<unknown>;
interface ProcessingResult<T> {
input: T;
type: string;
result: unknown;
timestamp: Date;
}
// Union 型に対応した Overload
function processInput(
data: string
): ProcessingResult<string>;
function processInput(
data: number
): ProcessingResult<number>;
function processInput(
data: object
): ProcessingResult<object>;
function processInput(
data: Array<unknown>
): ProcessingResult<Array<unknown>>;
function processInput(
data: InputData
): ProcessingResult<InputData> {
const timestamp = new Date();
if (typeof data === 'string') {
return {
input: data,
type: 'string',
result: data.toUpperCase(),
timestamp,
};
} else if (typeof data === 'number') {
return {
input: data,
type: 'number',
result: data * 2,
timestamp,
};
} else if (Array.isArray(data)) {
return {
input: data,
type: 'array',
result: data.length,
timestamp,
};
} else {
return {
input: data,
type: 'object',
result: Object.keys(data).length,
timestamp,
};
}
}
// 使用例
const stringResult = processInput('hello'); // ProcessingResult<string>
const numberResult = processInput(42); // ProcessingResult<number>
const arrayResult = processInput([1, 2, 3]); // ProcessingResult<Array<unknown>>
const objectResult = processInput({ key: 'value' }); // ProcessingResult<object>
判別可能な Union 型での型安全性
typescript// 判別可能な Union 型を活用した Overload
interface ApiSuccessResponse {
status: 'success';
data: unknown;
}
interface ApiErrorResponse {
status: 'error';
error: {
code: string;
message: string;
};
}
interface ApiLoadingResponse {
status: 'loading';
progress?: number;
}
type ApiResponse =
| ApiSuccessResponse
| ApiErrorResponse
| ApiLoadingResponse;
// 判別可能な Union での Overload
function handleResponse(
response: ApiSuccessResponse
): unknown;
function handleResponse(response: ApiErrorResponse): Error;
function handleResponse(
response: ApiLoadingResponse
): Promise<unknown>;
function handleResponse(
response: ApiResponse
): unknown | Error | Promise<unknown> {
switch (response.status) {
case 'success':
console.log('Data received:', response.data);
return response.data;
case 'error':
console.error('API Error:', response.error.message);
return new Error(
`${response.error.code}: ${response.error.message}`
);
case 'loading':
console.log(
'Loading...',
response.progress ? `${response.progress}%` : ''
);
return new Promise((resolve) => {
setTimeout(() => resolve(null), 1000);
});
default:
const exhaustiveCheck: never = response;
throw new Error(
`Unhandled response type: ${exhaustiveCheck}`
);
}
}
Builder Pattern 風 Overload(段階的な設定)
Builder パターンのような段階的な設定を Overload で実現するパターンです。
段階的なオブジェクト構築
typescript// Builder Pattern を模した Overload
interface HttpRequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
timeout?: number;
}
interface PartialConfig {
url?: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
timeout?: number;
}
class HttpRequest {
private config: PartialConfig = {};
// URL 設定の Overload
url(url: string): HttpRequest;
url(): string | undefined;
url(url?: string): HttpRequest | string | undefined {
if (url !== undefined) {
this.config.url = url;
return this;
}
return this.config.url;
}
// メソッド設定の Overload
method(
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
): HttpRequest;
method(): string | undefined;
method(
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
): HttpRequest | string | undefined {
if (method !== undefined) {
this.config.method = method;
return this;
}
return this.config.method;
}
// ヘッダー設定の Overload
headers(headers: Record<string, string>): HttpRequest;
headers(key: string, value: string): HttpRequest;
headers(): Record<string, string> | undefined;
headers(
keyOrHeaders?: string | Record<string, string>,
value?: string
): HttpRequest | Record<string, string> | undefined {
if (keyOrHeaders === undefined) {
return this.config.headers;
}
if (typeof keyOrHeaders === 'string') {
this.config.headers = this.config.headers || {};
this.config.headers[keyOrHeaders] = value!;
return this;
} else {
this.config.headers = {
...this.config.headers,
...keyOrHeaders,
};
return this;
}
}
// リクエスト実行
async execute(): Promise<Response> {
if (!this.config.url) {
throw new Error('URL is required');
}
const finalConfig: HttpRequestConfig = {
url: this.config.url,
method: this.config.method || 'GET',
headers: this.config.headers,
body: this.config.body,
timeout: this.config.timeout || 5000,
};
return fetch(finalConfig.url, {
method: finalConfig.method,
headers: finalConfig.headers,
body: finalConfig.body
? JSON.stringify(finalConfig.body)
: undefined,
});
}
}
// 使用例
async function builderExample() {
const response = await new HttpRequest()
.url('/api/users')
.method('POST')
.headers({ 'Content-Type': 'application/json' })
.headers('Authorization', 'Bearer token123')
.execute();
const data = await response.json();
return data;
}
ライブラリ風 API 設計実践
実際のライブラリ開発で活用できる、Function Overloads を使った API 設計パターンを具体的に見ていきましょう。
データフェッチングライブラリの設計
現代の Web アプリケーションで必須のデータフェッチング機能を、Function Overloads で設計します。
多様なフェッチパターンへの対応
typescript// データフェッチングライブラリの設計
interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
timeout?: number;
retry?: number;
}
interface FetchResponse<T> {
data: T;
status: number;
headers: Record<string, string>;
}
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
hasNext: boolean;
};
}
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(
baseUrl: string,
defaultHeaders: Record<string, string> = {}
) {
this.baseUrl = baseUrl;
this.defaultHeaders = defaultHeaders;
}
// 基本的な GET リクエスト
get<T>(endpoint: string): Promise<FetchResponse<T>>;
// オプション付き GET リクエスト
get<T>(
endpoint: string,
options: FetchOptions
): Promise<FetchResponse<T>>;
// ページネーション対応 GET リクエスト
get<T>(
endpoint: string,
options: FetchOptions & { paginated: true }
): Promise<PaginatedResponse<T>>;
async get<T>(
endpoint: string,
options?: FetchOptions & { paginated?: boolean }
): Promise<FetchResponse<T> | PaginatedResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
const config: RequestInit = {
method: 'GET',
headers: {
...this.defaultHeaders,
...options?.headers,
},
};
const response = await this.executeRequest(
url,
config,
options
);
const data = await response.json();
if (options?.paginated) {
return {
data: data.items,
pagination: {
page: data.page || 1,
limit: data.limit || 20,
total: data.total || 0,
hasNext: data.hasNext || false,
},
} as PaginatedResponse<T>;
}
return {
data,
status: response.status,
headers: this.parseHeaders(response.headers),
} as FetchResponse<T>;
}
// POST リクエストの Overload
post<T>(
endpoint: string,
body: unknown
): Promise<FetchResponse<T>>;
post<T>(
endpoint: string,
body: unknown,
options: FetchOptions
): Promise<FetchResponse<T>>;
async post<T>(
endpoint: string,
body: unknown,
options?: FetchOptions
): Promise<FetchResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
const config: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.defaultHeaders,
...options?.headers,
},
body: JSON.stringify(body),
};
const response = await this.executeRequest(
url,
config,
options
);
const data = await response.json();
return {
data,
status: response.status,
headers: this.parseHeaders(response.headers),
};
}
private async executeRequest(
url: string,
config: RequestInit,
options?: FetchOptions
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, options?.timeout || 5000);
try {
const response = await fetch(url, {
...config,
signal: controller.signal,
});
if (
!response.ok &&
options?.retry &&
options.retry > 0
) {
await new Promise((resolve) =>
setTimeout(resolve, 1000)
);
return this.executeRequest(url, config, {
...options,
retry: options.retry - 1,
});
}
return response;
} finally {
clearTimeout(timeoutId);
}
}
private parseHeaders(
headers: Headers
): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
}
// 使用例
async function fetchExample() {
const client = new ApiClient('https://api.example.com');
// 基本的な GET
const user = await client.get<User>('/users/123');
// オプション付き GET
const userWithOptions = await client.get<User>(
'/users/123',
{
headers: { Accept: 'application/json' },
timeout: 10000,
}
);
// ページネーション対応
const users = await client.get<User>('/users', {
paginated: true,
});
// POST リクエスト
const newUser = await client.post<User>('/users', {
name: 'John Doe',
email: 'john@example.com',
});
}
バリデーションライブラリの設計
型安全で柔軟なバリデーションライブラリを Function Overloads で設計します。
段階的バリデーション機能
typescript// バリデーションライブラリの設計
type ValidationRule<T> = (value: T) => boolean | string;
type ValidationResult = {
isValid: boolean;
errors: string[];
};
interface FieldValidator<T> {
rules: ValidationRule<T>[];
required: boolean;
}
class FormValidator {
private validators: Map<string, FieldValidator<unknown>> =
new Map();
// 単一フィールドのバリデーション設定
field<T>(name: string): FieldValidatorBuilder<T>;
// 既存フィールドの取得
field<T>(
name: string,
getter: true
): FieldValidator<T> | undefined;
field<T>(
name: string,
getter?: boolean
):
| FieldValidatorBuilder<T>
| FieldValidator<T>
| undefined {
if (getter) {
return this.validators.get(name) as
| FieldValidator<T>
| undefined;
}
return new FieldValidatorBuilder<T>(name, this);
}
// オブジェクト全体のバリデーション
validate<T extends Record<string, unknown>>(
data: T
): ValidationResult;
// 特定フィールドのバリデーション
validate<T>(
fieldName: string,
value: T
): ValidationResult;
validate<T>(
dataOrFieldName: T | string,
value?: unknown
): ValidationResult {
if (typeof dataOrFieldName === 'string') {
// 単一フィールドのバリデーション
return this.validateField(dataOrFieldName, value);
} else {
// オブジェクト全体のバリデーション
return this.validateObject(
dataOrFieldName as Record<string, unknown>
);
}
}
private validateField(
fieldName: string,
value: unknown
): ValidationResult {
const validator = this.validators.get(fieldName);
if (!validator) {
return { isValid: true, errors: [] };
}
const errors: string[] = [];
if (
validator.required &&
(value === undefined ||
value === null ||
value === '')
) {
errors.push(`${fieldName} is required`);
return { isValid: false, errors };
}
if (
value !== undefined &&
value !== null &&
value !== ''
) {
for (const rule of validator.rules) {
const result = rule(value);
if (result !== true) {
errors.push(
typeof result === 'string'
? result
: `${fieldName} is invalid`
);
}
}
}
return { isValid: errors.length === 0, errors };
}
private validateObject(
data: Record<string, unknown>
): ValidationResult {
const allErrors: string[] = [];
for (const [fieldName, validator] of this.validators) {
const fieldResult = this.validateField(
fieldName,
data[fieldName]
);
allErrors.push(...fieldResult.errors);
}
return {
isValid: allErrors.length === 0,
errors: allErrors,
};
}
setValidator<T>(
name: string,
validator: FieldValidator<T>
): void {
this.validators.set(
name,
validator as FieldValidator<unknown>
);
}
}
class FieldValidatorBuilder<T> {
private fieldValidator: FieldValidator<T> = {
rules: [],
required: false,
};
constructor(
private fieldName: string,
private parentValidator: FormValidator
) {}
// 必須フィールド設定
required(): FieldValidatorBuilder<T>;
required(isRequired: boolean): FieldValidatorBuilder<T>;
required(
isRequired: boolean = true
): FieldValidatorBuilder<T> {
this.fieldValidator.required = isRequired;
return this;
}
// カスタムルール追加
rule(
validator: ValidationRule<T>
): FieldValidatorBuilder<T>;
rule(
validator: ValidationRule<T>,
errorMessage: string
): FieldValidatorBuilder<T>;
rule(
validator: ValidationRule<T>,
errorMessage?: string
): FieldValidatorBuilder<T> {
if (errorMessage) {
this.fieldValidator.rules.push((value: T) => {
const result = validator(value);
return result === true ? true : errorMessage;
});
} else {
this.fieldValidator.rules.push(validator);
}
return this;
}
// 文字列専用バリデーション(型制約付き)
minLength(
length: number
): T extends string ? FieldValidatorBuilder<T> : never {
if (typeof '' as T extends string ? T : never) {
this.fieldValidator.rules.push((value: T) => {
const str = value as unknown as string;
return (
str.length >= length ||
`Minimum length is ${length}`
);
});
}
return this as T extends string
? FieldValidatorBuilder<T>
: never;
}
maxLength(
length: number
): T extends string ? FieldValidatorBuilder<T> : never {
if (typeof '' as T extends string ? T : never) {
this.fieldValidator.rules.push((value: T) => {
const str = value as unknown as string;
return (
str.length <= length ||
`Maximum length is ${length}`
);
});
}
return this as T extends string
? FieldValidatorBuilder<T>
: never;
}
// 数値専用バリデーション(型制約付き)
min(
minimum: number
): T extends number ? FieldValidatorBuilder<T> : never {
if (typeof 0 as T extends number ? T : never) {
this.fieldValidator.rules.push((value: T) => {
const num = value as unknown as number;
return (
num >= minimum || `Minimum value is ${minimum}`
);
});
}
return this as T extends number
? FieldValidatorBuilder<T>
: never;
}
max(
maximum: number
): T extends number ? FieldValidatorBuilder<T> : never {
if (typeof 0 as T extends number ? T : never) {
this.fieldValidator.rules.push((value: T) => {
const num = value as unknown as number;
return (
num <= maximum || `Maximum value is ${maximum}`
);
});
}
return this as T extends number
? FieldValidatorBuilder<T>
: never;
}
// バリデーター登録
build(): FormValidator {
this.parentValidator.setValidator(
this.fieldName,
this.fieldValidator
);
return this.parentValidator;
}
}
// 使用例
const validator = new FormValidator()
.field<string>('name')
.required()
.minLength(2)
.maxLength(50)
.build()
.field<string>('email')
.required()
.rule(
(email: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
'Invalid email format'
)
.build()
.field<number>('age')
.required()
.min(0)
.max(120)
.build();
// バリデーション実行
const userForm = {
name: 'John',
email: 'john@example.com',
age: 25,
};
const result = validator.validate(userForm); // 全体バリデーション
const nameResult = validator.validate('name', 'J'); // 単一フィールドバリデーション
イベント処理ライブラリの設計
型安全なイベント処理を実現するライブラリを設計します。
型安全なイベントエミッター
typescript// イベント処理ライブラリの設計
type EventListener<T = unknown> = (
data: T
) => void | Promise<void>;
type EventMap = Record<string, unknown>;
interface EventEmitterOptions {
maxListeners?: number;
async?: boolean;
}
class TypedEventEmitter<T extends EventMap> {
private listeners: Map<
keyof T,
Set<EventListener<T[keyof T]>>
> = new Map();
private options: Required<EventEmitterOptions>;
constructor(options: EventEmitterOptions = {}) {
this.options = {
maxListeners: options.maxListeners || 10,
async: options.async || false,
};
}
// イベントリスナー登録の Overload
on<K extends keyof T>(
event: K,
listener: EventListener<T[K]>
): this;
on<K extends keyof T>(
event: K,
listener: EventListener<T[K]>,
options: { once?: boolean }
): this;
on<K extends keyof T>(
event: K,
listener: EventListener<T[K]>,
options: { once?: boolean } = {}
): this {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
const listenerSet = this.listeners.get(event)!;
if (listenerSet.size >= this.options.maxListeners) {
console.warn(
`Max listeners (${
this.options.maxListeners
}) exceeded for event: ${String(event)}`
);
}
if (options.once) {
const onceWrapper: EventListener<T[K]> = (
data: T[K]
) => {
listenerSet.delete(onceWrapper);
return listener(data);
};
listenerSet.add(onceWrapper);
} else {
listenerSet.add(listener);
}
return this;
}
// 一回限りのリスナー登録
once<K extends keyof T>(
event: K,
listener: EventListener<T[K]>
): this {
return this.on(event, listener, { once: true });
}
// イベント発火の Overload
emit<K extends keyof T>(event: K, data: T[K]): boolean;
emit<K extends keyof T>(
event: K,
data: T[K],
options: { async: true }
): Promise<boolean>;
emit<K extends keyof T>(
event: K,
data: T[K],
options: { async?: boolean } = {}
): boolean | Promise<boolean> {
const listenerSet = this.listeners.get(event);
if (!listenerSet || listenerSet.size === 0) {
return false;
}
const useAsync = options.async || this.options.async;
if (useAsync) {
return this.emitAsync(event, data, listenerSet);
} else {
return this.emitSync(event, data, listenerSet);
}
}
private emitSync<K extends keyof T>(
event: K,
data: T[K],
listenerSet: Set<EventListener<T[K]>>
): boolean {
for (const listener of listenerSet) {
try {
listener(data);
} catch (error) {
console.error(
`Error in listener for event ${String(event)}:`,
error
);
}
}
return true;
}
private async emitAsync<K extends keyof T>(
event: K,
data: T[K],
listenerSet: Set<EventListener<T[K]>>
): Promise<boolean> {
const promises = Array.from(listenerSet).map(
async (listener) => {
try {
await listener(data);
} catch (error) {
console.error(
`Error in async listener for event ${String(
event
)}:`,
error
);
}
}
);
await Promise.all(promises);
return true;
}
// リスナー削除の Overload
off<K extends keyof T>(event: K): this;
off<K extends keyof T>(
event: K,
listener: EventListener<T[K]>
): this;
off<K extends keyof T>(
event?: K,
listener?: EventListener<T[K]>
): this {
if (event === undefined) {
// 全イベントのリスナーを削除
this.listeners.clear();
} else if (listener === undefined) {
// 特定イベントの全リスナーを削除
this.listeners.delete(event);
} else {
// 特定のリスナーを削除
const listenerSet = this.listeners.get(event);
if (listenerSet) {
listenerSet.delete(listener);
if (listenerSet.size === 0) {
this.listeners.delete(event);
}
}
}
return this;
}
// リスナー数の取得
listenerCount<K extends keyof T>(event?: K): number {
if (event === undefined) {
return Array.from(this.listeners.values()).reduce(
(total, set) => total + set.size,
0
);
}
return this.listeners.get(event)?.size || 0;
}
}
// 使用例
interface AppEvents {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'product:created': {
productId: string;
name: string;
price: number;
};
error: { message: string; code: number };
}
const eventEmitter = new TypedEventEmitter<AppEvents>();
// 型安全なイベントリスナー登録
eventEmitter.on('user:login', (data) => {
// data は { userId: string; timestamp: Date } 型
console.log(
`User ${data.userId} logged in at ${data.timestamp}`
);
});
eventEmitter.once('product:created', async (data) => {
// data は { productId: string; name: string; price: number } 型
await saveToAnalytics(data);
});
// 型安全なイベント発火
eventEmitter.emit('user:login', {
userId: 'user123',
timestamp: new Date(),
});
// 非同期発火
await eventEmitter.emit(
'product:created',
{
productId: 'prod456',
name: 'New Product',
price: 99.99,
},
{ async: true }
);
エラーハンドリングと型安全性
Function Overloads を使用する際の適切なエラーハンドリングと、型安全性を保つためのベストプラクティスを解説します。
Overload 使用時の型エラー対策
適切なシグネチャ順序の設計
typescript// ❌ 問題のあるシグネチャ順序
function problematicOverload(input: unknown): string;
function problematicOverload(input: string): string; // 到達不可能
function problematicOverload(input: number): number; // 到達不可能
// ✅ 適切なシグネチャ順序
function correctOverload(input: string): string;
function correctOverload(input: number): number;
function correctOverload(input: boolean): boolean;
function correctOverload(
input: unknown
): string | number | boolean;
function correctOverload(
input: unknown
): string | number | boolean {
if (typeof input === 'string') return input.toUpperCase();
if (typeof input === 'number') return input * 2;
if (typeof input === 'boolean') return !input;
throw new Error('Unsupported input type');
}
型アサーションとガードの適切な使用
typescript// 型ガードを活用した安全な Overload
interface User {
type: 'user';
id: string;
name: string;
}
interface Admin {
type: 'admin';
id: string;
name: string;
permissions: string[];
}
type UserOrAdmin = User | Admin;
// 型ガード関数
function isUser(entity: UserOrAdmin): entity is User {
return entity.type === 'user';
}
function isAdmin(entity: UserOrAdmin): entity is Admin {
return entity.type === 'admin';
}
// 型安全な Overload 実装
function processEntity(entity: User): string;
function processEntity(entity: Admin): string[];
function processEntity(
entity: UserOrAdmin
): string | string[] {
if (isUser(entity)) {
return `User: ${entity.name}`;
} else if (isAdmin(entity)) {
return entity.permissions;
} else {
// never 型による網羅性チェック
const exhaustiveCheck: never = entity;
throw new Error(
`Unhandled entity type: ${exhaustiveCheck}`
);
}
}
実行時エラーとコンパイル時エラーの使い分け
カスタムエラークラスの設計
typescript// Overload 専用のエラークラス
export class OverloadError extends Error {
constructor(
message: string,
public readonly expectedTypes: string[],
public readonly receivedType: string,
public readonly receivedValue: unknown
) {
super(message);
this.name = 'OverloadError';
}
static createTypeError(
functionName: string,
expectedTypes: string[],
receivedValue: unknown
): OverloadError {
const receivedType = typeof receivedValue;
const message =
`${functionName}: Expected one of [${expectedTypes.join(
', '
)}], ` +
`but received ${receivedType}: ${receivedValue}`;
return new OverloadError(
message,
expectedTypes,
receivedType,
receivedValue
);
}
}
// エラーハンドリングを含む Overload 実装
function safeProcessData(data: string): string;
function safeProcessData(data: number): number;
function safeProcessData(data: boolean): boolean;
function safeProcessData(
data: string | number | boolean
): string | number | boolean {
try {
if (typeof data === 'string') {
if (data.length === 0) {
throw new OverloadError(
'Empty string not allowed',
['non-empty string'],
'string',
data
);
}
return data.toUpperCase();
} else if (typeof data === 'number') {
if (!Number.isFinite(data)) {
throw new OverloadError(
'Infinite or NaN numbers not allowed',
['finite number'],
'number',
data
);
}
return data * 2;
} else if (typeof data === 'boolean') {
return !data;
} else {
throw OverloadError.createTypeError(
'safeProcessData',
['string', 'number', 'boolean'],
data
);
}
} catch (error) {
if (error instanceof OverloadError) {
console.error('Overload Error:', error.message);
throw error;
}
throw new OverloadError(
`Unexpected error in safeProcessData: ${error}`,
['string', 'number', 'boolean'],
typeof data,
data
);
}
}
Result パターンによるエラーハンドリング
typescript// Result パターンを活用した型安全なエラーハンドリング
type Result<T, E = Error> =
| {
success: true;
data: T;
}
| {
success: false;
error: E;
};
// Result を返す Overload
function tryProcessData(
data: string
): Result<string, OverloadError>;
function tryProcessData(
data: number
): Result<number, OverloadError>;
function tryProcessData(
data: boolean
): Result<boolean, OverloadError>;
function tryProcessData(
data: string | number | boolean
): Result<string | number | boolean, OverloadError> {
try {
const result = safeProcessData(data);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error:
error instanceof OverloadError
? error
: new OverloadError(
'Unknown error occurred',
['string', 'number', 'boolean'],
typeof data,
data
),
};
}
}
// 使用例
function handleResults() {
const stringResult = tryProcessData('hello');
if (stringResult.success) {
console.log('String result:', stringResult.data); // string 型
} else {
console.error(
'String error:',
stringResult.error.message
);
}
const numberResult = tryProcessData(42);
if (numberResult.success) {
console.log('Number result:', numberResult.data); // number 型
} else {
console.error(
'Number error:',
numberResult.error.message
);
}
}
デバッグしやすい Overload 設計
デバッグ情報の充実
typescript// デバッグ情報を含む Overload 設計
interface DebugInfo {
functionName: string;
selectedOverload: string;
inputTypes: string[];
timestamp: Date;
}
class OverloadTracker {
private static instance: OverloadTracker;
private callHistory: DebugInfo[] = [];
static getInstance(): OverloadTracker {
if (!OverloadTracker.instance) {
OverloadTracker.instance = new OverloadTracker();
}
return OverloadTracker.instance;
}
track(info: DebugInfo): void {
this.callHistory.push(info);
if (this.callHistory.length > 100) {
this.callHistory.shift(); // 履歴を100件に制限
}
}
getHistory(): DebugInfo[] {
return [...this.callHistory];
}
getLastCall(): DebugInfo | undefined {
return this.callHistory[this.callHistory.length - 1];
}
}
// デバッグ対応 Overload
function debuggableProcess(data: string): string;
function debuggableProcess(data: number): number;
function debuggableProcess(data: boolean): boolean;
function debuggableProcess(
data: string | number | boolean
): string | number | boolean {
const tracker = OverloadTracker.getInstance();
let selectedOverload: string;
let result: string | number | boolean;
if (typeof data === 'string') {
selectedOverload = 'string overload';
result = data.toUpperCase();
} else if (typeof data === 'number') {
selectedOverload = 'number overload';
result = data * 2;
} else {
selectedOverload = 'boolean overload';
result = !data;
}
// デバッグ情報の記録
tracker.track({
functionName: 'debuggableProcess',
selectedOverload,
inputTypes: [typeof data],
timestamp: new Date(),
});
return result;
}
// デバッグユーティリティ関数
function printOverloadHistory(): void {
const tracker = OverloadTracker.getInstance();
const history = tracker.getHistory();
console.table(
history.map((info) => ({
Function: info.functionName,
Overload: info.selectedOverload,
InputTypes: info.inputTypes.join(', '),
Timestamp: info.timestamp.toISOString(),
}))
);
}
// 使用例とデバッグ
debuggableProcess('hello');
debuggableProcess(42);
debuggableProcess(true);
printOverloadHistory(); // 実行履歴をテーブル形式で表示
まとめ:Function Overloads による開発体験の向上
TypeScript の Function Overloads は、従来の JavaScript では実現困難だった柔軟で型安全な API 設計を可能にする強力な機能です。本記事で紹介したパターンとベストプラクティスを活用することで、以下のような大きなメリットを実現できます。
Function Overloads の主要な価値
# | 価値 | 具体的な効果 | 適用シーン |
---|---|---|---|
1 | 型安全性の向上 | 引数パターンに応じた適切な戻り値型の推論 | ライブラリ API、ユーティリティ関数 |
2 | 開発者体験の改善 | IDE での正確なオートコンプリートと型チェック | チーム開発、大規模プロジェクト |
3 | API の柔軟性確保 | 単一の関数名で多様な使用パターンをサポート | 公開ライブラリ、フレームワーク |
4 | 保守性の向上 | 明確な型定義による意図の伝達と変更の安全性 | 長期保守プロジェクト |
5 | 学習コストの削減 | 直感的な API 設計による使いやすさの向上 | 新規メンバーの学習、ドキュメント化 |
実装における重要なポイント
1. 適切なシグネチャ設計
Function Overloads の効果を最大化するには、シグネチャの順序と具体性のバランスが重要です。より具体的なシグネチャから順番に定義し、最後に最も汎用的なシグネチャを配置することで、TypeScript コンパイラが適切なシグネチャを選択できます。
2. エラーハンドリングの戦略
型安全性を保ちながら、実行時エラーに対する適切な対応策を用意することが重要です。カスタムエラークラスや Result パターンを活用することで、エラー情報を構造化し、デバッグしやすい設計を実現できます。
3. パフォーマンスへの配慮
Function Overloads はコンパイル時の型チェック機能であり、実行時のパフォーマンスに直接的な影響は与えません。しかし、複雑な型推論や大量のシグネチャはコンパイル時間に影響する可能性があるため、適切なバランスを保つことが重要です。
今後の発展と活用領域
Function Overloads の技術は、TypeScript の進化と共にさらなる発展が期待されます:
- AI アシスタントとの連携: より精密な型推論による開発支援の向上
- フレームワーク統合: Next.js、React、Vue などでの標準的な活用パターンの確立
- ツールチェーンの進化: ESLint、Prettier、TypeDoc などでの専用サポート
本記事で紹介した実践的なパターンを活用して、より型安全で保守性の高い TypeScript コードを実現してください。Function Overloads は、単なる技術的な機能を超えて、チーム全体の開発体験を根本的に改善する力を持っています。
あなたのプロジェクトでも、今日から Function Overloads を活用して、次世代の API 設計を体験してみてはいかがでしょうか。
関連リンク
- TypeScript Handbook - Function Overloads
- TypeScript Deep Dive - Function Overloading
- Advanced TypeScript - Function Overloads Best Practices
- TypeScript Function Overloads - GitHub Discussion
- Effective TypeScript - Item 50: Prefer Function Declarations to Function Expressions
- TypeScript API Design Guidelines
- review
もう三日坊主とはサヨナラ!『続ける思考』井上新八
- review
チーム開発が劇的に変わった!『リーダブルコード』Dustin Boswell & Trevor Foucher
- review
アジャイル初心者でも大丈夫!『アジャイルサムライ − 達人開発者への道』Jonathan Rasmusson
- review
人生が作品になる!『自分の中に毒を持て』岡本太郎
- review
体調不良の 99%が解決!『眠れなくなるほど面白い 図解 自律神経の話』小林弘幸著で学ぶ、現代人必須の自律神経コントロール術と人生を変える健康革命
- review
衝撃の事実!『睡眠こそ最強の解決策である』マシュー・ウォーカー著が明かす、99%の人が知らない睡眠の驚くべき真実と人生を変える科学的メカニズム