Пошук уроків, статей та іншого контенту
Створюйте кілька сигнатур однієї функції для безпечної роботи з різними наборами аргументів.
Перевантаження функцій дає змогу описати кілька способів виклику однієї функції. Для кожного набору аргументів можна вказати власну сигнатуру та тип результату.
Це особливо корисно, коли функція:
приймає різну кількість аргументів;
підтримує різні типи аргументів;
повертає різні типи залежно від аргументів;
має кілька логічно пов’язаних варіантів використання.
Розглянемо функцію, яка читає значення конфігурації:
const config = {
host: "localhost",
port: "3000",
protocol: "https",
};
function readConfig(key: string): string | undefined;
function readConfig(keys: readonly string[]): Record<string, string>;
function readConfig(
keyOrKeys: string | readonly string[],
): string | undefined | Record<string, string> {
if (typeof keyOrKeys === "string") {
return config[keyOrKeys as keyof typeof config];
}
const result: Record<string, string> = {};
for (const key of keyOrKeys) {
const value = config[key as keyof typeof config];
if (value !== undefined) {
result[key] = value;
}
}
return result;
}
const host = readConfig("host");
// string | undefined
const selected = readConfig(["host", "protocol"]);
// Record<string, string>
console.log(host);
console.log(selected);У цього прикладу є дві доступні сигнатури:
function readConfig(key: string): string | undefined;
function readConfig(keys: readonly string[]): Record<string, string>;Тому TypeScript розуміє:
виклик із рядком повертає string | undefined;
виклик із масивом повертає Record<string, string>.
Перевантаження складається з трьох частин:
сигнатури перевантаження;
ще однієї або кількох сигнатур перевантаження;
реалізації функції.
function functionName(argument: string): string;
function functionName(argument: number): number;
function functionName(argument: string | number): string | number {
// реалізація
}Перші два оголошення описують публічний API функції. Третє оголошення містить тіло функції та реалізацію.
Сигнатури перевантаження не мають тіла:
function parseValue(value: string): number;
function parseValue(value: number): number;Вони повідомляють TypeScript, які виклики дозволені та який результат вони мають.
Сигнатура реалізації має тіло:
function parseValue(value: string | number): number {
if (typeof value === "string") {
return Number(value);
}
return value;
}Вона повинна бути достатньо широкою, щоб обробити всі варіанти, описані перевантаженнями.
Водночас сигнатура реалізації не є доступною для виклику напряму. TypeScript використовує для перевірки викликів лише сигнатури перевантаження.
function combine(value: string): string;
function combine(value: number): number;
function combine(value: string | number): string | number {
return value;
}
combine("text"); // string
combine(42); // number
// Помилка: цей варіант не описаний сигнатурами перевантаження
combine(true);Перевантаження особливо корисне, коли тип результату залежить від типу аргументу.
function toArray(value: string): string[];
function toArray(value: number): number[];
function toArray(value: string | number): string[] | number[] {
if (typeof value === "string") {
return value.split("");
}
return [value];
}
const letters = toArray("TypeScript");
// string[]
const numbers = toArray(10);
// number[]Без перевантаження довелося б використовувати один об’єднаний тип:
function toArrayWithoutOverloads(
value: string | number,
): string[] | number[] {
return typeof value === "string" ? value.split("") : [value];
}
const result = toArrayWithoutOverloads("TypeScript");
// string[] | number[]У другому випадку TypeScript не пов’язує конкретний аргумент із конкретним типом результату так точно.
Перевантаження може описувати не лише різні типи, а й різну кількість аргументів.
Наприклад, функція створює URL:
function buildUrl(path: string): URL;
function buildUrl(path: string, query: Record<string, string>): URL;
function buildUrl(
path: string,
query?: Record<string, string>,
): URL {
const url = new URL(path, "https://example.com");
if (query) {
for (const [key, value] of Object.entries(query)) {
url.searchParams.set(key, value);
}
}
return url;
}
const pageUrl = buildUrl("/users");
// URL
const filteredUrl = buildUrl("/users", {
role: "admin",
status: "active",
});
// URL
console.log(pageUrl.href);
console.log(filteredUrl.href);Обидва виклики є дозволеними, але спосіб їх опису для користувача функції залишається чітким.
TypeScript перевіряє сигнатури перевантаження зверху вниз і використовує першу сумісну сигнатуру.
function format(value: string): "string";
function format(value: string | number): "string" | "number";
function format(value: string | number): "string" | "number" {
return typeof value === "string" ? "string" : "number";
}
const result = format("hello");
// "string"Перший варіант є точнішим, тому він має бути перед загальнішим.
Неправильний порядок може приховати точнішу сигнатуру:
function formatValue(value: string | number): "string" | "number";
function formatValue(value: string): "string";
function formatValue(value: string | number): "string" | "number" {
return typeof value === "string" ? "string" : "number";
}
const result = formatValue("hello");
// "string" | "number"У цьому випадку TypeScript знаходить загальну сигнатуру першою та не переходить до точнішої.
Розташовуйте сигнатури:
від найспецифічнішої;
до найзагальнішої.
function getValue(key: "name"): string;
function getValue(key: "age"): number;
function getValue(key: string): string | number | undefined;Сигнатура реалізації повинна бути сумісною з усіма сигнатурами перевантаження.
function getLength(value: string): number;
function getLength(value: readonly unknown[]): number;
function getLength(value: string | readonly unknown[]): number {
return value.length;
}Реалізація приймає об’єднання всіх можливих типів аргументів і повертає тип, який охоплює всі результати.
Помилковий приклад:
function getLength(value: string): number;
function getLength(value: readonly unknown[]): number;
function getLength(value: string | readonly unknown[]): string {
return String(value.length);
}Сигнатури перевантаження обіцяють number, але реалізація повертає string. Така функція не пройде перевірку TypeScript.
Тип реалізації не повинен бути вужчим за типи, описані в перевантаженнях.
Усередині реалізації TypeScript бачить типи параметрів, оголошені в сигнатурі реалізації. Тому для роботи з конкретним варіантом потрібно виконати звуження типу.
function repeat(value: string, count: number): string;
function repeat(value: string[], count: number): string[];
function repeat(
value: string | string[],
count: number,
): string | string[] {
if (typeof value === "string") {
return value.repeat(count);
}
return value.flatMap((item) => Array(count).fill(item));
}
const text = repeat("ab", 3);
// string
const items = repeat(["a", "b"], 2);
// string[]Перевірка typeof value === "string" звужує тип у першій гілці до string, а в іншій гілці TypeScript розуміє, що залишився тип string[].
Не кожна функція потребує перевантаження. Якщо всі варіанти мають однаковий тип результату, часто достатньо об’єднаного типу.
function logValue(value: string | number): void {
console.log(value);
}Перевантаження доречніше, коли воно зберігає залежність між аргументами та результатом:
function getDefault(value: string): string;
function getDefault(value: number): number;
function getDefault(value: string | number): string | number {
return value;
}Об’єднання типів може бути простішим, але менш точним:
function getDefaultUnion(value: string | number): string | number {
return value;
}
const value = getDefaultUnion("hello");
// string | numberУ першому варіанті результат для рядка відомий як string, а в другому TypeScript зберігає весь об’єднаний тип.
Використовуйте об’єднаний тип, якщо:
логіка функції однакова для всіх варіантів;
тип результату не залежить від типу аргументу;
кілька перевантажень не додають точності;
сигнатури перевантаження стали надто складними.
function printId(id: string | number): string {
return `ID: ${id}`;
}Перевантаження виправдане, якщо:
різні набори аргументів мають різну семантику;
тип результату залежить від набору аргументів;
потрібно заборонити конкретні комбінації параметрів;
публічний API має кілька чітких сценаріїв використання.
Перевантажувати можна функції, які приймають об’єкти різної форми.
type User = {
id: number;
name: string;
};
type UserInput = {
name: string;
};
function createUser(input: UserInput): User;
function createUser(input: User): User;
function createUser(input: UserInput | User): User {
if ("id" in input) {
return input;
}
return {
id: Math.floor(Math.random() * 1_000_000),
name: input.name,
};
}
const existingUser = createUser({
id: 1,
name: "Олена",
});
const newUser = createUser({
name: "Андрій",
});
console.log(existingUser);
console.log(newUser);Перевантаження тут описують два різні сценарії:
передано вже створеного користувача;
передано дані для створення нового користувача.
Методи класів також можуть мати кілька сигнатур.
class Cache {
private readonly values = new Map<string, string>();
set(key: string, value: string): this;
set(values: Record<string, string>): this;
set(
keyOrValues: string | Record<string, string>,
value?: string,
): this {
if (typeof keyOrValues === "string") {
if (value === undefined) {
throw new Error("Для ключа потрібно передати значення");
}
this.values.set(keyOrValues, value);
return this;
}
for (const [key, item] of Object.entries(keyOrValues)) {
this.values.set(key, item);
}
return this;
}
get(key: string): string | undefined {
return this.values.get(key);
}
}
const cache = new Cache()
.set("theme", "dark")
.set({
language: "uk",
layout: "compact",
});
console.log(cache.get("theme"));
console.log(cache.get("language"));Тип this у результаті дає змогу безпечно викликати методи ланцюжком.
Стрілкова функція не підтримує кілька сигнатур у такому самому синтаксисі, як звичайна функція:
// Так записати не можна:
// const convert = (value: string): number;
// const convert = (value: number): string;Для стрілкової функції можна описати перевантаження через тип із кількома сигнатурами виклику:
type Convert = {
(value: string): number;
(value: number): string;
};
const convert: Convert = (value: string | number): string | number => {
if (typeof value === "string") {
return Number(value);
}
return String(value);
};
const numberValue = convert("42");
// number
const stringValue = convert(42);
// string
console.log(numberValue);
console.log(stringValue);Тип Convert описує доступні способи виклику, а реалізація обробляє обидва варіанти.
Дженерики часто допомагають зберегти зв’язок між типом аргументу та типом результату без великої кількості перевантажень.
function identity<T>(value: T): T {
return value;
}
const text = identity("hello");
// string
const count = identity(42);
// numberАле дженерик не завжди замінює перевантаження. Перевантаження зручніше, коли потрібно описати різні правила для різних наборів аргументів.
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user = {
name: "Олена",
age: 28,
};
const name = getProperty(user, "name");
// string
const age = getProperty(user, "age");
// numberУ цьому прикладі дженерик добре описує залежність між ключем і значенням. Якщо ж різні набори аргументів мають різну поведінку, перевантаження може бути зрозумілішим для користувачів API.
Порівняйте два підходи.
type RequestOptions =
| {
method: "GET";
url: string;
}
| {
method: "POST";
url: string;
body: string;
};
function request(options: RequestOptions): void {
console.log(options.method, options.url);
}function request(method: "GET", url: string): void;
function request(method: "POST", url: string, body: string): void;
function request(
method: "GET" | "POST",
url: string,
body?: string,
): void {
console.log(method, url, body);
}
request("GET", "/users");
request("POST", "/users", '{"name":"Олена"}');Обидва підходи можуть бути правильними. Перевантаження добре підходить, коли виклики мають різну кількість аргументів і хочеться описати їх у звичному позиційному форматі.
function calculate(value: number): number;
function calculate(value: string, radix: number): number;
function calculate(
value: number | string,
radix?: number,
): number {
return typeof value === "number"
? value * 2
: Number.parseInt(value, radix ?? 10);
}
// Помилка: немає перевантаження для двох числових аргументів
// calculate(10, 2);Те, що реалізація технічно могла б прийняти певний виклик, не робить цей виклик доступним. Його потрібно явно додати до сигнатур перевантаження.
function process(value: string): string;
function process(value: number): number;
// Помилка: реалізація не охоплює number
// function process(value: string): string {
// return value;
// }Реалізація повинна підтримувати всі оголошені варіанти.
function read(value: string | number): string | number;
function read(value: string): string;
function read(value: string | number): string | number {
return value;
}Перша сигнатура вже підходить для рядка, тому друга сигнатура фактично не використовується. Спочатку розміщуйте точніші варіанти.
function parse(value: string): number;
function parse(value: number): number;
function parse(value: string | number): number {
return typeof value === "string" ? Number(value) : value;
}Тип string | number тут є частиною реалізації, а не окремим доступним сценарієм. Якщо потрібно дозволити виклик із типом, якого немає серед перевантажень, додайте відповідну сигнатуру явно.
Велика кількість майже однакових сигнатур ускладнює підтримку коду. Перед додаванням нового перевантаження перевірте, чи не достатньо:
об’єднаного типу;
дженерика;
об’єкта параметрів із дискримінованим об’єднанням.
Перевантаження має описувати окремий зрозумілий сценарій, а не кожну незначну варіацію типів.
Перевантаження описує кілька способів виклику однієї функції.
Сигнатури перевантаження розміщують перед реалізацією.
Сигнатура реалізації не доступна користувачам напряму.
Реалізація повинна бути сумісною з усіма перевантаженнями.
Усередині реалізації потрібно звужувати об’єднані типи.
Точніші сигнатури мають розташовуватися перед загальнішими.
Перевантаження особливо корисне, коли тип результату залежить від аргументів.
Якщо поведінка та тип результату однакові для всіх варіантів, часто достатньо об’єднаного типу.
Для стрілкових функцій перевантаження можна описати через тип із кількома сигнатурами виклику.