Function Overloads в TypeScript
Что такое Function Overloads?
Function Overloads (Перегрузка функций) — это возможность объявить несколько сигнатур для одной функции. TypeScript выберет правильную сигнатуру на основе переданных аргументов.
Базовый синтаксис
// Сигнатуры перегрузок
function greet(name: string): string;
function greet(firstName: string, lastName: string): string;
// Реализация (implementation signature)
function greet(firstName: string, lastName?: string): string {
if (lastName) {
return `Hello, ${firstName} ${lastName}!`;
}
return `Hello, ${firstName}!`;
}
// Использование
greet('John'); // "Hello, John!"
greet('John', 'Doe'); // "Hello, John Doe!"
// greet('John', 'Doe', 'Jr.'); // Ошибка: Expected 1-2 arguments
Зачем нужны перегрузки?
Разные типы возвращаемого значения
function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: 'active'): boolean;
function getValue(key: string): string | number | boolean {
const data = {
name: 'John',
age: 30,
active: true
};
return data[key as keyof typeof data];
}
const name = getValue('name'); // string
const age = getValue('age'); // number
const active = getValue('active'); // boolean
Связанные типы аргументов и результата
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'button'): HTMLButtonElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const div = createElement('div'); // HTMLDivElement
const span = createElement('span'); // HTMLSpanElement
const button = createElement('button'); // HTMLButtonElement
Опциональные параметры с разными типами
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
if (month !== undefined && day !== undefined) {
return new Date(yearOrTimestamp, month, day);
}
return new Date(yearOrTimestamp);
}
const date1 = makeDate(1234567890); // Date
const date2 = makeDate(2024, 11, 19); // Date
// const date3 = makeDate(2024, 11); // Ошибка
Порядок сигнатур имеет значение
TypeScript проверяет сигнатуры сверху вниз и использует первую подходящую.
// Неправильно - общая сигнатура первая
function process(value: any): any;
function process(value: string): string;
function process(value: number): number;
function process(value: any): any {
return value;
}
const result = process('hello'); // any (не string!)
// Правильно - от конкретных к общим
function process(value: string): string;
function process(value: number): number;
function process(value: any): any;
function process(value: any): any {
return value;
}
const result = process('hello'); // string
Implementation Signature
Реализация должна быть совместима со ВСЕМИ сигнатурами перегрузок.
// Сигнатуры перегрузок
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// Реализация должна охватывать оба случая
function combine(a: string | number, b: string | number): string | number {
if (typeof a === 'string' && typeof b === 'string') {
return a + b;
}
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
throw new Error('Invalid arguments');
}
const str = combine('Hello, ', 'World'); // string
const num = combine(10, 20); // number
Важно:
Implementation signature НЕ видна снаружи. Пользователи видят только overload signatures.
Перегрузки vs Union Types
Когда использовать перегрузки
// С перегрузками - тип зависит от аргумента
function parse(data: string): object;
function parse(data: object): string;
function parse(data: string | object): string | object {
if (typeof data === 'string') {
return JSON.parse(data);
}
return JSON.stringify(data);
}
const obj = parse('{"name":"John"}'); // object
const str = parse({ name: 'John' }); // string
Когда Union Types достаточно
// Union type проще и понятнее
function format(value: string | number): string {
return value.toString();
}
Практические примеры
Извлечение из массива
function getFirst<T>(arr: T[]): T | undefined;
function getFirst<T>(arr: T[], count: number): T[];
function getFirst<T>(arr: T[], count?: number): T | T[] | undefined {
if (count === undefined) {
return arr[0];
}
return arr.slice(0, count);
}
const numbers = [1, 2, 3, 4, 5];
const first = getFirst(numbers); // number | undefined
const firstThree = getFirst(numbers, 3); // number[]
Поиск элементов
function find(predicate: (item: string) => boolean): string | undefined;
function find(predicate: (item: string) => boolean, all: true): string[];
function find(
predicate: (item: string) => boolean,
all?: boolean
): string | string[] | undefined {
const items = ['apple', 'banana', 'cherry'];
if (all) {
return items.filter(predicate);
}
return items.find(predicate);
}
const one = find(item => item.startsWith('a')); // string | undefined
const many = find(item => item.startsWith('a'), true); // string[]
Event Listener
function addEventListener(
element: HTMLElement,
event: 'click',
handler: (e: MouseEvent) => void
): void;
function addEventListener(
element: HTMLElement,
event: 'keypress',
handler: (e: KeyboardEvent) => void
): void;
function addEventListener(
element: HTMLElement,
event: string,
handler: (e: Event) => void
): void {
element.addEventListener(event, handler);
}
const button = document.querySelector('button')!;
addEventListener(button, 'click', (e) => {
// e: MouseEvent
console.log(e.clientX);
});
addEventListener(button, 'keypress', (e) => {
// e: KeyboardEvent
console.log(e.key);
});
API Wrapper
function request(method: 'GET', url: string): Promise<Response>;
function request(
method: 'POST' | 'PUT',
url: string,
body: object
): Promise<Response>;
function request(
method: string,
url: string,
body?: object
): Promise<Response> {
const options: RequestInit = { method };
if (body) {
options.body = JSON.stringify(body);
options.headers = { 'Content-Type': 'application/json' };
}
return fetch(url, options);
}
// Использование
request('GET', '/api/users');
request('POST', '/api/users', { name: 'John' });
// request('POST', '/api/users'); // Ошибка: body обязателен
Перегрузки методов класса
class DataStore {
private data: Record<string, any> = {};
// Перегрузки методов
get(key: 'name'): string;
get(key: 'age'): number;
get(key: 'active'): boolean;
get(key: string): any {
return this.data[key];
}
set(key: 'name', value: string): void;
set(key: 'age', value: number): void;
set(key: 'active', value: boolean): void;
set(key: string, value: any): void {
this.data[key] = value;
}
}
const store = new DataStore();
store.set('name', 'John'); // OK
store.set('age', 30); // OK
// store.set('age', 'thirty'); // Ошибка
const name = store.get('name'); // string
const age = store.get('age'); // number
Перегрузки конструкторов
class Point {
x: number;
y: number;
constructor(x: number, y: number);
constructor(coords: { x: number; y: number });
constructor(xOrCoords: number | { x: number; y: number }, y?: number) {
if (typeof xOrCoords === 'number') {
this.x = xOrCoords;
this.y = y!;
} else {
this.x = xOrCoords.x;
this.y = xOrCoords.y;
}
}
}
const p1 = new Point(10, 20);
const p2 = new Point({ x: 10, y: 20 });
Generics в перегрузках
function map<T, U>(arr: T[], fn: (item: T) => U): U[];
function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[];
function map<T, U>(
arr: T[],
fn: (item: T, index?: number) => U
): U[] {
return arr.map(fn as any);
}
const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2);
const indexed = map(numbers, (n, i) => `${i}: ${n}`);
Типичные ошибки
Несовместимая реализация
// Ошибка: реализация не совместима с перегрузками
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean { // Ошибка!
return value;
}
Забыть про порядок
// Плохо - общий случай перекрывает конкретные
function handle(value: any): any;
function handle(value: string): string; // Недостижимо!
function handle(value: any): any {
return value;
}
Слишком много перегрузок
// Плохо - слишком сложно
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
function format(value: Date): string;
function format(value: object): string;
// ... еще 10 перегрузок
// Лучше использовать union или generic
function format(value: string | number | boolean | Date | object): string {
return String(value);
}
Best Practices
Используйте перегрузки только когда необходимо
// Не нужны перегрузки
function add(a: number, b: number): number {
return a + b;
}
// Нужны перегрузки - разные типы результата
function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: string): any {
// ...
}
Держите перегрузки близко к реализации
// Хорошо - все вместе
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
return value;
}
Документируйте сложные перегрузки
/**
* Creates a date from timestamp
* @param timestamp - Unix timestamp in milliseconds
*/
function makeDate(timestamp: number): Date;
/**
* Creates a date from year, month, and day
* @param year - Full year (e.g., 2024)
* @param month - Month (0-11)
* @param day - Day of month (1-31)
*/
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
// ...
}
Альтернативы перегрузкам
Conditional Types
type ReturnType<T> = T extends 'string' ? string :
T extends 'number' ? number :
T extends 'boolean' ? boolean :
never;
function getValue<T extends 'string' | 'number' | 'boolean'>(
type: T
): ReturnType<T> {
// ...
return null as any;
}
Discriminated Unions
type Options =
| { type: 'GET'; url: string }
| { type: 'POST'; url: string; body: object };
function request(options: Options): Promise<Response> {
// ...
}
Вывод
Function Overloads:
- Позволяют объявить несколько сигнатур для одной функции
- Выбор сигнатуры зависит от аргументов
- Порядок сигнатур важен (от конкретных к общим)
- Implementation signature должна быть совместима со всеми перегрузками
- Полезны когда тип результата зависит от аргументов
- Альтернативы: union types, conditional types, discriminated unions
- Не злоупотребляйте — используйте только когда необходимо
На собеседовании:
Важно уметь:
- Объяснить синтаксис перегрузки функций
- Написать пример с разными типами результата
- Объяснить разницу между overload и implementation signatures
- Показать важность порядка сигнатур
- Рассказать когда перегрузки лучше union types
- Назвать альтернативы (conditional types, discriminated unions)