関数の応用
この章では、TypeScriptにおける関数のより高度な機能について学びます。
この章で学ぶこと
- 関数のオーバーロード
- コールバック関数の型付け
- レストパラメータの使い方
- thisの型付け
この章のコード例は、TypeScript Playgroundで試すことができます。
関数のオーバーロード
関数のオーバーロードとは、同じ名前の関数で異なる型のパラメータや戻り値を扱う機能です。TypeScriptでは、複数の「オーバーロードシグネチャ」と1つの「実装シグネチャ」を定義します。
オーバーロードの基本
// オーバーロードシグネチャ(宣言だけ)
function reverse(value: string): string;
function reverse(value: number): number;
// 実装シグネチャ(実際の実装)
function reverse(value: string | number): string | number {
if (typeof value === 'string') {
return value.split('').reverse().join('');
} else {
return Number(value.toString().split('').reverse().join(''));
}
}
// 使用例
console.log(reverse('hello')); // 'olleh' (string型を返す)
console.log(reverse(12345)); // 54321 (number型を返す)
コード解説:
- オーバーロードシグネチャ: 関数の異なる呼び出し方を宣言
- 実装シグネチャ: すべてのオーバーロードを満たす実際の実装
- TypeScriptは呼び出し時に適切なオーバーロードを選択
より複雑なオーバーロード
引数の数が異なるオーバーロードも可能です。
// 引数の数が異なるオーバーロード
function createElement(tag: string): HTMLElement;
function createElement(tag: string, content: string): HTMLElement;
function createElement(tag: string, content: string, className: string): HTMLElement;
function createElement(
tag: string,
content?: string,
className?: string
): HTMLElement {
const element = document.createElement(tag);
if (content) {
element.textContent = content;
}
if (className) {
element.className = className;
}
return element;
}
// 使用例
const div1 = createElement('div');
const div2 = createElement('div', 'Hello');
const div3 = createElement('div', 'Hello', 'container');
実践的なオーバーロード例
// ユーザーを検索する関数: IDまたは名前で検索可能
type User = { id: number; name: string; email: string };
function findUser(id: number): User | undefined;
function findUser(name: string): User | undefined;
function findUser(idOrName: number | string): User | undefined {
// ダミーデータ
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
];
if (typeof idOrName === 'number') {
return users.find((user) => user.id === idOrName);
} else {
return users.find((user) => user.name === idOrName);
}
}
// 使用例
console.log(findUser(1)); // { id: 1, name: 'Alice', ... }
console.log(findUser('Bob')); // { id: 2, name: 'Bob', ... }
オーバーロードの注意点
// 実装シグネチャはオーバーロードシグネチャを包含する必要がある
function add(a: number, b: number): number;
function add(a: string, b: string): string;
// 実装シグネチャは両方の型をサポートする必要がある
function add(a: number | string, b: number | string): number | string {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else {
return String(a) + String(b);
}
}
console.log(add(5, 3)); // 8
console.log(add('Hello, ', 'World')); // 'Hello, World'
2026年の潮流: オーバーロードより先にジェネリクス制約を検討する
関数オーバーロードは型推論の仕組みが複雑で、実装と型定義が乖離しやすいという欠点があります。以下のようなケースでは、オーバーロードを使わずにジェネリクス制約で同じ型安全性を実現できます。
// ❌ オーバーロードで書く場合
function firstOrDefault(arr: string[], defaultValue: string): string;
function firstOrDefault(arr: number[], defaultValue: number): number;
function firstOrDefault<T>(arr: T[], defaultValue: T): T {
return arr[0] ?? defaultValue
}
// ✅ ジェネリクス制約で書けば宣言も短い
function firstOrDefault<T>(arr: T[], defaultValue: T): T {
return arr[0] ?? defaultValue
}
オーバーロードが必要になるのは、引数の数や組み合わせで戻り値の型が大きく変わる場合(例: DOM の querySelector がタグ名で型を変える)です。単に引数の型バリエーションがあるだけなら、ジェネリクスや conditional types で表現するほうが保守性が高くなります。
コールバック関数
コールバック関数とは、他の関数に引数として渡される関数のことです。非同期処理やイベント処理でよく使用されます。
コールバック関数の型付け
// コールバック関数を受け取る関数の型定義
function processNumbers(
numbers: number[],
callback: (num: number) => number
): number[] {
return numbers.map(callback);
}
// 使用例
const doubled = processNumbers([1, 2, 3, 4, 5], (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
const squared = processNumbers([1, 2, 3, 4, 5], (n) => n * n);
console.log(squared); // [1, 4, 9, 16, 25]
コード解説:
callback: (num: number) => number: コールバック関数の型(num: number): 引数としてnumber型を1つ受け取る=> number: 戻り値としてnumber型を返す
型エイリアスを使ったコールバック定義
// コールバック関数の型を型エイリアスで定義
type NumberProcessor = (num: number) => number;
type StringProcessor = (str: string) => string;
function processItems<T>(
items: T[],
processor: (item: T) => T
): T[] {
return items.map(processor);
}
// number配列の処理
const numbers = processItems([1, 2, 3], (n) => n * 2);
console.log(numbers); // [2, 4, 6]
// string配列の処理
const strings = processItems(['a', 'b', 'c'], (s) => s.toUpperCase());
console.log(strings); // ['A', 'B', 'C']
非同期コールバック
// 成功/失敗のコールバックを受け取る関数
function fetchData(
url: string,
onSuccess: (data: unknown) => void,
onError: (error: Error) => void
): void {
fetch(url)
.then((response) => response.json())
.then((data) => onSuccess(data))
.catch((error) => onError(error));
}
// 使用例
fetchData(
'https://api.example.com/data',
(data) => {
console.log('Success:', data);
},
(error) => {
console.error('Error:', error.message);
}
);
イベントハンドラのコールバック
// イベントコールバックの型定義
type EventCallback = (event: Event) => void;
function addClickListener(
element: HTMLElement,
callback: EventCallback
): void {
element.addEventListener('click', callback);
}
// 使用例
const button = document.getElementById('myButton') as HTMLElement;
addClickListener(button, (event) => {
console.log('Button clicked', event);
});
複数のコールバックを持つ関数
// 処理の各段階でコールバックを呼び出す関数
type ProgressCallback = (progress: number) => void;
type CompleteCallback = (result: string) => void;
type ErrorCallback = (error: Error) => void;
function processWithCallbacks(
data: string[],
onProgress: ProgressCallback,
onComplete: CompleteCallback,
onError: ErrorCallback
): void {
try {
const total = data.length;
const results: string[] = [];
data.forEach((item, index) => {
// 各アイテムを処理
results.push(item.toUpperCase());
// 進捗を報告
onProgress(Math.round(((index + 1) / total) * 100));
});
// 完了を報告
onComplete(results.join(', '));
} catch (error) {
onError(error as Error);
}
}
// 使用例
processWithCallbacks(
['apple', 'banana', 'cherry'],
(progress) => console.log(`Progress: ${progress}%`),
(result) => console.log(`Complete: ${result}`),
(error) => console.error(`Error: ${error.message}`)
);
// Progress: 33%
// Progress: 67%
// Progress: 100%
// Complete: APPLE, BANANA, CHERRY
レストパラメータ
レストパラメータ(Rest Parameters)は、可変長の引数を配列として受け取る機能です。
基本的な使い方
// レストパラメータの基本構文: ...パラメータ名: 型[]
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
console.log(sum(5)); // 5
console.log(sum()); // 0
コード解説:
...numbers: number[]: 任意の数のnumber型引数を配列として受け取る- 内部では通常の配列として操作可能
通常のパラメータとの組み合わせ
// 最初のパラメータは通常、残りはレストパラメータ
function multiply(multiplier: number, ...numbers: number[]): number[] {
return numbers.map((n) => n * multiplier);
}
console.log(multiply(2, 1, 2, 3, 4)); // [2, 4, 6, 8]
console.log(multiply(10, 5, 10, 15)); // [50, 100, 150]
// 複数の通常パラメータとレストパラメータ
function createMessage(
prefix: string,
suffix: string,
...words: string[]
): string {
return `${prefix} ${words.join(' ')} ${suffix}`;
}
console.log(createMessage('[', ']', 'Hello', 'World'));
// '[ Hello World ]'
レストパラメータの注意点
// レストパラメータは最後のパラメータでなければならない
// 正しい
function correct(multiplier: number, ...numbers: number[]) {
return numbers.map((n) => n * multiplier);
}
// 間違い: レストパラメータの後に他のパラメータがある
// function wrong(...numbers: number[], multiplier: number) {} // Error
// レストパラメータは1つだけ
// 間違い: 複数のレストパラメータは使えない
// function wrong(...a: number[], ...b: string[]) {} // Error
実践的な使用例
// 例1: ログ出力関数
function log(level: string, ...messages: string[]): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}]`, ...messages);
}
log('INFO', 'Application started');
log('ERROR', 'Something went wrong:', 'Connection failed');
// 例2: 複数の値の最大値を求める
function max(...numbers: number[]): number {
if (numbers.length === 0) {
return -Infinity;
}
return Math.max(...numbers);
}
console.log(max(5, 10, 3, 8, 15)); // 15
console.log(max(42)); // 42
// 例3: 文字列の結合
function joinStrings(separator: string, ...strings: string[]): string {
return strings.join(separator);
}
console.log(joinStrings(', ', 'Apple', 'Banana', 'Orange'));
// 'Apple, Banana, Orange'
console.log(joinStrings(' - ', 'Tokyo', 'Osaka', 'Kyoto'));
// 'Tokyo - Osaka - Kyoto'
// 例4: 配列をフラット化
function flatten<T>(...arrays: T[][]): T[] {
return arrays.reduce((acc, arr) => [...acc, ...arr], []);
}
console.log(flatten([1, 2], [3, 4], [5, 6])); // [1, 2, 3, 4, 5, 6]
console.log(flatten(['a'], ['b', 'c'])); // ['a', 'b', 'c']
thisの型付け
TypeScriptでは、関数内のthisの型を明示的に指定できます。
アロー関数とthis
アロー関数ではthisがレキシカルに束縛されます(定義時のthisを保持)。
class Counter {
count: number = 0;
// 従来の関数: thisが呼び出し元によって変わる可能性がある
incrementWrong() {
setTimeout(function() {
// this.count++; // Error: thisがCounterを指さない可能性
console.log('Wrong');
}, 1000);
}
// アロー関数: thisが定義時のコンテキスト(Counter)を保持
incrementCorrect() {
setTimeout(() => {
this.count++; // OK: thisがCounterインスタンスを指す
console.log(this.count);
}, 1000);
}
}
const counter = new Counter();
counter.incrementCorrect(); // 1秒後に1が表示される
thisパラメータ
関数の最初のパラメータとしてthisを指定することで、thisの型を明示できます。
interface User {
name: string;
greet(this: User): void;
}
const user: User = {
name: 'Alice',
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
user.greet(); // "Hello, I'm Alice"
// 誤った呼び出し方をするとエラー
const greetFunc = user.greet;
// greetFunc(); // Error: The 'this' context... is not assignable
クラスでのthis
class EventHandler {
message: string = 'Button clicked';
setupEventListener() {
const button = document.getElementById('myButton');
// アロー関数を使う(thisが正しく動作)
button?.addEventListener('click', () => {
console.log(this.message); // 'Button clicked'
});
}
}
class NumberProcessor {
multiplier: number = 2;
processNumbers(numbers: number[]): number[] {
// アロー関数でthis.multiplierにアクセス
return numbers.map((n) => n * this.multiplier);
}
}
const processor = new NumberProcessor();
console.log(processor.processNumbers([1, 2, 3])); // [2, 4, 6]
試してみよう: 汎用的なフィルター関数を作ろう ★★★
以下の要件を満たす汎用的なフィルター関数群を作成してください。
要件:
filterNumbers(numbers, ...predicates): 複数の条件関数を受け取り、すべての条件を満たす数値だけを返すfilterByRange(min, max): 指定した範囲内の数値かどうかを判定する関数を返すfilterByDivisible(divisor): 指定した数で割り切れるかどうかを判定する関数を返すcreateFilter(predicate): 条件関数を受け取り、配列をフィルタリングする関数を返す
ヒント
filterNumbersではレストパラメータで複数の条件関数を受け取るfilterByRangeとfilterByDivisibleは高階関数(関数を返す関数)として実装createFilterはジェネリクスを使って任意の型に対応させる- 条件関数の型は
(value: number) => boolean
回答と解説
// 条件関数の型を定義
type Predicate<T> = (value: T) => boolean;
// 複数の条件関数を受け取り、すべての条件を満たす数値だけを返す
function filterNumbers(
numbers: number[],
...predicates: Predicate<number>[]
): number[] {
return numbers.filter((num) =>
// すべての条件関数がtrueを返す場合のみtrueを返す
predicates.every((predicate) => predicate(num))
);
}
// 指定した範囲内の数値かどうかを判定する関数を返す
function filterByRange(min: number, max: number): Predicate<number> {
return (value: number) => value >= min && value <= max;
}
// 指定した数で割り切れるかどうかを判定する関数を返す
function filterByDivisible(divisor: number): Predicate<number> {
return (value: number) => value % divisor === 0;
}
// 条件関数を受け取り、配列をフィルタリングする関数を返す
function createFilter<T>(predicate: Predicate<T>): (items: T[]) => T[] {
return (items: T[]) => items.filter(predicate);
}
// テスト
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 18, 20];
// filterNumbersのテスト: 5以上15以下で、3で割り切れる数
const result1 = filterNumbers(
numbers,
filterByRange(5, 15),
filterByDivisible(3)
);
console.log(result1); // [6, 9, 12, 15]
// filterByRangeのテスト
const inRange = filterByRange(5, 10);
console.log(inRange(7)); // true
console.log(inRange(15)); // false
// filterByDivisibleのテスト
const isEven = filterByDivisible(2);
console.log(isEven(4)); // true
console.log(isEven(5)); // false
// createFilterのテスト
const filterPositive = createFilter<number>((n) => n > 0);
console.log(filterPositive([-2, -1, 0, 1, 2])); // [1, 2]
const filterLongStrings = createFilter<string>((s) => s.length > 3);
console.log(filterLongStrings(['a', 'ab', 'abc', 'abcd', 'abcde'])); // ['abcd', 'abcde']
解説:
Predicate<T>: 汎用的な条件関数の型。値を受け取ってbooleanを返すfilterNumbers: レストパラメータで複数の条件関数を受け取り、everyですべての条件をチェックfilterByRange: クロージャを使ってminとmaxを保持する条件関数を返すfilterByDivisible: 同様にクロージャでdivisorを保持createFilter: ジェネリクスを使って任意の型の配列に対応
高階関数のメリット:
- 関数を組み合わせて複雑なフィルタリングを実現
- 条件をパラメータ化して再利用可能に
- コードの意図が明確になる
まとめ
この章で学んだこと:
- 関数のオーバーロード: 同じ関数名で異なる型のパラメータ/戻り値を扱う
- コールバック関数: 関数を引数として渡し、処理の流れを制御する
- レストパラメータ:
...を使って可変長の引数を配列として受け取る - thisの型付け: アロー関数やthisパラメータでthisの型を明示する
次の章では、クラスの基本について学びます。TypeScriptのオブジェクト指向プログラミングの基礎を扱います。
初学者がつまずきやすいポイント
よくある間違い
❌ オーバーロードの順序を間違える
// ❌ 間違い: より具体的なシグネチャを後に書いている
function process(value: string | number): string;
function process(value: string): string; // これは呼ばれない
// ✅ 正しい: より具体的なシグネチャを先に書く
function process(value: string): string;
function process(value: string | number): string;
function process(value: string | number): string {
return String(value);
}
原因: TypeScriptは上から順にシグネチャをチェックし、最初にマッチしたものを使用します。
解決策: 具体的なシグネチャを先に、汎用的なシグネチャを後に配置してください。
❌ コールバック関数の型が不正確
// ❌ 間違い: コールバックの引数の型を省略
function processItems(items: string[], callback) {
items.forEach(callback);
}
// ✅ 正しい: コールバックの型を明示
function processItems(
items: string[],
callback: (item: string, index: number) => void
): void {
items.forEach(callback);
}
原因: 引数の型が省略されると暗黙的にanyになります。
解決策: コールバック関数の引数と戻り値の型を明示的に定義してください。
❌ レストパラメータを最後以外に配置
// ❌ 間違い: レストパラメータが途中にある
function wrong(...numbers: number[], multiplier: number) {
// Error: A rest parameter must be last in a parameter list
}
// ✅ 正しい: レストパラメータは最後に配置
function correct(multiplier: number, ...numbers: number[]) {
return numbers.map(n => n * multiplier);
}
原因: JavaScriptの仕様上、レストパラメータは引数リストの最後である必要があります。
解決策: レストパラメータは必ず関数引数の最後に配置してください。
❌ thisのコンテキストを見失う
class Timer {
seconds: number = 0;
// ❌ 間違い: 通常の関数でthisを使用
startWrong() {
setInterval(function() {
this.seconds++; // Error: thisがTimerを指さない
}, 1000);
}
// ✅ 正しい: アロー関数でthisを保持
startCorrect() {
setInterval(() => {
this.seconds++; // OK: アロー関数はthisを保持
}, 1000);
}
}
原因: 通常の関数は呼び出し元によってthisが変わりますが、アロー関数は定義時のthisを保持します。
解決策: コールバック内でthisを使う場合はアロー関数を使用してください。
❌ 実装シグネチャを直接呼び出せると思う
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string { // 実装シグネチャ
return String(value);
}
// ✅ OK: オーバーロードシグネチャに一致
format('hello'); // OK
format(123); // OK
// ❌ 間違い: 実装シグネチャは直接呼び出せない
format({ toString: () => 'test' }); // Error: オーバーロードにマッチしない
原因: 実装シグネチャは型チェックには使われず、オーバーロードシグネチャのみが外部から見える型となります。
解決策: 呼び出し可能な型はオーバーロードシグネチャで定義してください。
次に読む
クラスと OOP へ進みます。クラス、継承、アクセス修飾子、抽象クラスを使ったオブジェクト指向設計を学びます。