Асинхронное программирование в TypeScript: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Асинхронное программирование в TypeScript: полное руководство для начинающих

Асинхронное программирование — одна из ключевых концепций в современной веб-разработке. Если вы только начинаете изучать TypeScript, понимание того, как работать с асинхронным кодом, откроет перед вами возможности для создания быстрых и отзывчивых приложений. В этом руководстве мы разберем основы асинхронного программирования в TypeScript: от простых колбэков до современных async/await, с акцентом на строгую типизацию, которую предоставляет TypeScript.

Что такое асинхронное программирование и зачем оно нужно?

В синхронном программировании код выполняется последовательно: каждая следующая операция ждет завершения предыдущей. Это может привести к "зависанию" интерфейса, если, например, нужно загрузить данные с сервера. Асинхронное программирование позволяет запускать длительные операции (запросы к API, чтение файлов, таймеры) в фоне, не блокируя основной поток выполнения. TypeScript, будучи надстройкой над JavaScript, полностью поддерживает асинхронные паттерны и добавляет к ним мощную систему типов.

Promise: основа асинхронности в TypeScript

Promise — это объект, представляющий результат асинхронной операции, который может быть в одном из трех состояний: pending (ожидание), fulfilled (выполнено успешно) или rejected (выполнено с ошибкой). В TypeScript мы можем указать тип данных, которые вернет промис, что делает код более предсказуемым.

Пример создания Promise с типизацией:

// Функция, возвращающая Promise с типом string
function fetchUserData(userId: number): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId > 0) {
                resolve(`Данные пользователя с ID ${userId}`);
            } else {
                reject(new Error("Некорректный ID пользователя"));
            }
        }, 1000);
    });
}

// Использование Promise
fetchUserData(123)
    .then((data: string) => {
        console.log(data); // "Данные пользователя с ID 123"
    })
    .catch((error: Error) => {
        console.error(error.message);
    });

Обратите внимание: мы явно указали, что промис возвращает string, а в .then() параметр data также имеет тип string. Это помогает избежать ошибок на этапе компиляции.

Цепочки Promise и обработка ошибок

Промисы можно объединять в цепочки, передавая результат одной асинхронной операции в другую. TypeScript будет автоматически выводить типы на каждом шаге.

Пример цепочки Promise:

interface User {
    id: number;
    name: string;
}

function getUser(id: number): Promise<User> {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ id, name: "Анна" }), 500);
    });
}

function getPosts(userId: number): Promise<string[]> {
    return new Promise((resolve) => {
        setTimeout(() => resolve(["Пост 1", "Пост 2"]), 500);
    });
}

// Цепочка вызовов
getUser(1)
    .then((user: User) => {
        console.log(`Пользователь: ${user.name}`);
        return getPosts(user.id); // Возвращаем новый Promise
    })
    .then((posts: string[]) => {
        console.log(`Посты: ${posts.join(", ")}`);
    })
    .catch((error: Error) => {
        console.error("Ошибка:", error.message);
    });

Если на любом этапе возникнет ошибка, выполнение перейдет к ближайшему .catch(). Это централизованная обработка ошибок — одно из главных преимуществ промисов перед колбэками.

Async/await: элегантный синтаксис асинхронности

Синтаксис async/await делает асинхронный код похожим на синхронный, что упрощает его чтение и поддержку. Ключевое слово async перед функцией указывает, что она возвращает Promise. Внутри такой функции можно использовать await, чтобы приостановить выполнение до завершения промиса.

Пример с async/await:

async function loadUserData(userId: number): Promise<void> {
    try {
        const user: User = await getUser(userId);
        console.log(`Пользователь: ${user.name}`);

        const posts: string[] = await getPosts(user.id);
        console.log(`Посты: ${posts.join(", ")}`);
    } catch (error) {
        console.error("Не удалось загрузить данные:", error);
    }
}

loadUserData(1);

Код стал более линейным и понятным. Обработка ошибок выполняется с помощью обычного блока try/catch. TypeScript автоматически определяет тип переменных user и posts на основе возвращаемых типов функций getUser и getPosts.

Важно: await можно использовать только внутри функций, объявленных с ключевым словом async. Тип возвращаемого значения такой функции всегда будет Promise<T>, где T — тип возвращаемого значения.

Типизация асинхронных функций и параллельное выполнение

TypeScript позволяет строго типизировать асинхронные функции, что особенно полезно в больших проектах. Вы можете создавать обобщенные (generic) асинхронные функции и запускать несколько промисов параллельно с помощью Promise.all.

Пример с Promise.all и типизацией:

// Обобщенная функция для имитации запроса
async function fetchData<T>(url: string, mockData: T): Promise<T> {
    return new Promise((resolve) => {
        setTimeout(() => resolve(mockData), 1000);
    });
}

// Параллельное выполнение нескольких запросов
async function loadDashboard(): Promise<void> {
    try {
        const [user, posts, comments] = await Promise.all([
            fetchData<User>("/user", { id: 1, name: "Иван" }),
            fetchData<string[]>("/posts", ["Пост 1", "Пост 2"]),
            fetchData<number[]>("/comments", [101, 102, 103])
        ]);

        console.log(user.name); // Иван
        console.log(posts.length); // 2
        console.log(comments); // [101,                

Блоги

Рекомендаций по книгам