Функции в TypeScript: параметры, возвращаемые значения, void, never и overloads

Функции в TypeScript: параметры, возвращаемые значения, void, never и overloads


Функции мы пишем постоянно.

Но TypeScript позволяет описать их гораздо точнее, чем обычный JavaScript.

Начнём с простого.


Типизация параметров

В JavaScript:

function sum(a, b) {
    return a + b;
}

JavaScript спокойно позволит:

sum(1, 2);

sum("hello", "world");

sum({}, []);

Для JavaScript это нормально.

Для TypeScript можно явно указать типы:

function sum(a: number, b: number) {
    return a + b;
}

Теперь:

sum(1, 2);

работает.

А:

sum("hello", "world");

даст ошибку.


Тип возвращаемого значения

TypeScript часто сам понимает, что возвращает функция:

function sum(a: number, b: number) {
    return a + b;
}

Он выводит:

number

Но иногда тип лучше указать явно:

function sum(
    a: number,
    b: number
): number {
    return a + b;
}

Особенно это полезно для публичных функций, сложной логики и API.

Например:

function getUser(id: number): User | null {
    // ...
}

Сразу понятно:

функция либо возвращает User,
либо ничего не находит и возвращает null

Необязательные параметры

Допустим:

function greet(name: string, age?: number) {
    // ...
}

Теперь можно вызвать:

greet("Tigran");

или:

greet("Tigran", 20);

Но внутри функции TypeScript понимает:

age

может быть:

number | undefined

Поэтому:

console.log(age.toFixed());

может вызвать ошибку.

Нужно сначала проверить:

if (age !== undefined) {
    console.log(age.toFixed());
}

Что такое void?

void обычно означает:

Функция не возвращает полезного значения.

Например:

function logMessage(message: string): void {
    console.log(message);
}

Функция что-то делает:

logMessage("Hello");

Но результат использовать нельзя:

const result = logMessage("Hello");

result будет иметь тип:

void

void часто встречается в callback-функциях.

Например:

button.addEventListener(
    "click",
    () => {
        console.log("Clicked");
    }
);

Нам важен сам факт выполнения функции.

Возвращаемое значение не нужно.


Что такое never?

never означает:

Эта функция никогда не завершится нормально.

Например:

function throwError(message: string): never {
    throw new Error(message);
}

Функция либо выбрасывает ошибку:

throwError("Something went wrong");

либо может бесконечно выполняться:

function infiniteLoop(): never {
    while (true) {
        // ...
    }
}

Разница между void и never:

void
↓
Функция завершилась,
но ничего полезного не вернула.

never
↓
Функция вообще не дошла до нормального завершения.

Например:

function log(): void {
    console.log("Hello");
}

После:

log();
console.log("This will run");

вторая строка выполнится.

А:

function fail(): never {
    throw new Error();
}

После:

fail();
console.log("This will run");

TypeScript понимает, что до второй строки выполнение не дойдёт.


Function overloads

Теперь немного интереснее.

Представим функцию:

function getUser(id: number): User {
    // ...
}

Но мы хотим поддерживать два способа вызова:

getUser(1);

и:

getUser("Tigran");

Причём результат может отличаться.

Например:

getUser(1);
// User

getUser("Tigran");
// User[]

Можно написать overloads:

function getUser(id: number): User;

function getUser(name: string): User[];

function getUser(
    value: number | string
): User | User[] {
    if (typeof value === "number") {
        return {
            id: value,
            name: "Tigran"
        };
    }

    return [
        {
            id: 1,
            name: value
        }
    ];
}

Теперь TypeScript понимает:

const user = getUser(1);

Здесь:

user

имеет тип:

User

А здесь:

const users = getUser("Tigran");

тип:

User[]

Зачем overloads, если есть union?

Можно было написать:

function getUser(
    value: number | string
): User | User[] {
    // ...
}

Но тогда:

const result = getUser(1);

будет иметь тип:

User | User[]

Хотя мы точно знаем:

если передали number
→ получим User

если передали string
→ получим User[]

Overloads позволяют TypeScript сохранить эту зависимость.


Когда overloads не нужны

Не стоит использовать overloads просто потому, что они существуют.

Иногда достаточно:

function print(value: string | number) {
    console.log(value);
}

Если логика простая, union type обычно лучше.

Overloads полезны, когда:

тип аргумента
↓
влияет на
↓
тип результата

Итого

Функция в TypeScript может описывать гораздо больше, чем просто:

function sum(a, b)

Мы можем указать:

function sum(
    a: number,
    b: number
): number

Сделать параметр необязательным:

age?: number

Сказать, что функция ничего полезного не возвращает:

void

Или вообще никогда нормально не завершится:

never

А если результат зависит от переданных аргументов, можно использовать:

function overloads

И именно в этом заключается одна из главных идей TypeScript.

Мы описываем не только какие данные существуют, но и как наш код может с ними взаимодействовать.

TypeScript постепенно превращается из «JavaScript с типами» в систему, которая описывает правила и контракты всего приложения.


Report Page