برنامه‌نویسی asynchronous روشی برای نوشتن کدی است که می‌تواند تسک‌ها را به صورت مستقل از یکدیگر اجرا کند، بدون اینکه نیاز باشد یک تسک قبل از شروع تسک دیگر به پایان برسد. وقتی به برنامه‌نویسی asynchronous فکر می‌کنیم، مفهوم multitasking و مدیریت مؤثر زمان به ذهنمان می‌آید.

در این مقاله قصد داریم تا مفهوم async/await در تایپ اسکریپت و برنامه‌نویسی asynchronous را باهم بررسی کنیم.

درک مفاهیم promiseها در تایپ اسکریپت

قبل از ورود به بحث  async/await، لازم است بدانیم که promiseها پایه و اساس برنامه‌نویسی asynchronous در تایپ اسکریپت و جاوااسکریپت هستند. یک promise نشان‌دهنده‌ مقداری است که ممکن است بلافاصله در دسترس نباشد، اما در آینده resolve خواهد شد. یک promise می‌تواند در یکی از سه حالت زیر باشد:

در تایپ اسکریپت، می‌توانیم promiseها را به شکل زیر ایجاد و استفاده کنیم:

// Type-safe Promise creation
interface ApiResponse {
  data: string;
  timestamp: number;
}

const fetchData = new Promise<ApiResponse>((resolve, reject) => {
  try {
    // Simulating API call
    setTimeout(() => {
      resolve({
        data: "Success!",
        timestamp: Date.now()
      });
    }, ۱۰۰۰);
  } catch (error) {
    reject(error);
  }
});

promiseها را می‌توانیم با استفاده از .then() برای عملیات موفق و .catch() برای مدیریت خطا به صورت زنجیره‌ای اجرا نماییم:

fetchData
  .then(response => {
    console.log(response.data);  // TypeScript knows response has ApiResponse type
    return response.timestamp;
  })
  .then(timestamp => {
    console.log(new Date(timestamp).toISOString());
  })
  .catch(error => {
    console.error('Error:', error);
  });

در ادامه، مفهوم promiseها را دوباره بررسی خواهیم کرد و نحوه اجرای عملیات asynchronous به صورت parallel را توضیح خواهیم داد.

معرفی async/await در تایپ اسکریپت

تایپ اسکریپت یک superset از جاوااسکریپت است، بنابراین async/await دقیقاً همان‌طور که در جاوااسکریپت کار می‌کند، در تایپ اسکریپت نیز عمل می‌کند؛ اما با ویژگی‌های اضافی و type safety. تایپ اسکریپت به ما این امکان را می‌دهد که type safety را برای نتیجه مورد انتظار تضمین کنیم و حتی خطاهای تایپ را بررسی نماییم. این ویژگی به ما کمک می‌کند تا اشکالات را زودتر در فرآیند توسعه شناسایی کنیم.

async/await در واقع یک سینتکس ساده‌سازی برای promiseها است، یعنی کلمات کلیدی async و await در واقع یک لایه پوششی روی promiseها هستند. یک تابع async همیشه یک promise را return می‌کند، حتی اگر مستقیماً از کلمه کلیدی Promise استفاده نکنیم، کامپایلر آن را در یک resolve promise شده قرار می‌دهد.

به عنوان مثال:

//Snippet 1
const myAsynFunction = async (url: string): Promise<T> => {
    const { data } = await fetch(url)
    return data
}

//Snippet 2
const immediatelyResolvedPromise = (url: string) => {
    const resultPromise = new Promise((resolve, reject) => {
        resolve(fetch(url))
    })
    return  resultPromise
}

اگرچه این دو نمونه کد ظاهری متفاوت دارند، اما در اصل عملکرد آن‌ها مشابه است.

async/await به ما این امکان را می‌دهد تا کدی که داریم را به صورت همگام‌تر بنویسیم و promise را در همان خط کد باز کنیم. این ویژگی هنگام کار با الگوهای پیچیده asynchronous بسیار مفید است.

برای استفاده درست و کامل از سینتکس async/await، داشتن درک پایه‌ای از promiseها ضروری می‌باشد.

بررسی دقیق‌تر promiseها در تایپ اسکریپت

همان‌طور که قبلاً به آن اشاره کردیم، promise نشان‌دهنده این است که چیزی در یک زمان مشخص اتفاق خواهد افتاد، و این امکان را فراهم می‌کند که برنامه از نتیجه آن رویداد در آینده برای انجام کارهای دیگر استفاده کند.

برای توضیح بهتر، یک مثال واقعی را بررسی کرده و آن را به شبه کد و سپس کد تایپ اسکریپت تبدیل می‌کنیم.

فرض کنید ما یک چمنزار داریم که باید چمن‌های آن را کوتاه کنیم. با یک شرکت تماس می‌گیریم که قول می‌دهد چمن‌ها را در عرض چند ساعت کوتاه کند. در مقابل، ما نیز قول می‌دهیم که بلافاصله پس از کوتاه شدن چمن‌ها هزینه را پرداخت نماییم، به شرطی که کار به درستی انجام شده باشد.

آیا الگوی خاصی را در این فرآیند مشاهده می‌کنیم؟ اولین نکته آشکار این است که رویداد دوم (پرداخت هزینه) کاملاً به رویداد اول (کوتاه شدن چمن‌ها) وابسته است. اگر promise رویداد اول انجام شود، promise رویداد بعدی اجرا خواهد شد. در این مرحله، promise می‌تواند resolve شود، reject شود، یا در حالت pending باقی بماند.

سینتکس promise

در این کد، ما دو promise تعریف کرده‌ایم: یکی که مربوط به شرکت است و دیگری مربوط به خودمان. promise شرکت بعد از ۱۰۰,۰۰۰ میلی‌ثانیه یا resolve می‌شود یا reject می‌گردد. هر Promise در یکی از سه وضعیت قرار دارد: resolved (انجام‌شده) اگر مشکلی وجود نداشته باشد، rejected (رد شده) اگر خطایی رخ دهد، یا pending (در انتظار) اگر هنوز تصمیم‌گیری نشده باشد. در مثال ما، این وضعیت بعد از ۱۰۰۰۰۰ میلی‌ثانیه مشخص خواهد شد.

// I send a request to the company. This is synchronous
// company replies with a promise
const angelMowersPromise = new Promise<string>((resolve, reject) => {
    // a resolved promise after certain hours
    setTimeout(() => {
        resolve('We finished mowing the lawn')
    }, ۱۰۰۰۰۰) // resolves after 100,000ms
    reject("We couldn't mow the lawn")
})

const myPaymentPromise = new Promise<Record<string, number | string>>((resolve, reject) => {
    // a resolved promise with  an object of 1000 Euro payment
    // and a thank you message
    setTimeout(() => {
        resolve({
            amount: 1000,
            note: 'Thank You',
        })
    }, ۱۰۰۰۰۰)
    // reject with 0 Euro and an unstatisfatory note
    reject({
        amount: 0,
        note: 'Sorry Lawn was not properly Mowed',
    })
})

اجرای متوالی با then

با استفاده از کلمه کلیدی then می‌توانیم promiseها را زنجیره‌ای اجرا کنیم. این روش مانند دستورات متوالی در زبان طبیعی است، «اول این کار را انجام بده، بعد آن کار و سپس … ».

کد زیر ابتدا angelMowersPromise را اجرا می‌کند. در صورتی که خطایی وجود نداشته باشد، myPaymentPromise اجرا می‌شود. در صورت بروز خطا در هر یک از این دو  promise، آن خطا در بلاک catch مدیریت خواهد شد:

angelMowersPromise
    .then(() => myPaymentPromise.then(res => console.log(res)))
    .catch(error => console.log(error))

یکی از کاربردهای رایج promiseها در برنامه‌نویسی فرانت‌اند، ارسال درخواست‌های شبکه‌ای و مدیریت پاسخ‌ها است. در کد زیر، یک درخواست برای دریافت لیست کارمندان از سرور ارسال شده است:

const api =  'http://dummy.restapiexample.com/api/v1/employees'
   fetch(api)
    .then(response => response.json())
    .then(employees => employees.forEach(employee => console.log(employee.id)) // logs all employee id
    .catch(error => console.log(error.message))) // logs any error from the promise

گاهی اوقات نیاز داریم چندین promise را همزمان یا به ترتیب اجرا کنیم. در این شرایط، Promise.all یا Promise.race ابزارهای مفیدی هستند. به عنوان مثال، فرض کنید قصد داریم لیست ۱۰۰۰ کاربر GitHub را دریافت کرده و سپس برای هر کدام، یک درخواست جداگانه برای دریافت آواتار ارسال کنیم. در این حالت، انتظار برای هر درخواست به صورت ترتیبی منطقی نیست، بلکه بهتر است همه آن‌ها را به طور همزمان اجرا نماییم. این موضوع را در بخش Promise.all بررسی خواهیم کرد.

درک async/await

سینتکس async/await کار با promiseها را در جاوااسکریپت ساده‌تر می‌کند. این امکان را فراهم می‌کند که بتوانیم promiseها را به روشی خوانا و مشابه کدهای synchronous بنویسیم.

یک تابع async/await همیشه یک Promise را return می‌کند. حتی اگر از کلمه کلیدی Promise استفاده نکنیم، کامپایلر به طور خودکار تابع را درون یک Promise resolve شده قرار می‌دهد. این ویژگی باعث می‌شود که مقدار بازگشتی از یک تابع async را مانند یک Promise در نظر بگیریم، که در مواقعی که نیاز به حل چندین عملیات asynchronous داریم، بسیار مفید است.

همان‌طور که از نام آن پیداست، async همیشه در کنار await استفاده می‌شود. به این معنی که فقط می‌توانیم از await درون یک تابع async استفاده کنیم. وجود async در ابتدای تابع به کامپایلر اطلاع می‌دهد که این یک تابع asynchronous است.

اگر بخواهیم promiseهای مثال بالا را به async/await تبدیل کنیم، سینتکس آن به شکل زیر خواهد بود:

const myAsync = async (): Promise<Record<string, number | string>> => {
    await angelMowersPromise
    const response = await myPaymentPromise
    return response
}

همان‌طور که مشاهده می‌کنیم، این روش خواناتر است و اجرای کد حس همگام بودن دارد. در اینجا، await منتظر اجرای angelMowersPromise می‌ماند و سپس myPaymentPromise اجرا می‌شود.

مدیریت خطا با try/catch

ما در این بخش، از مثال دریافت اطلاعات کارمند برای مشاهده نحوه مدیریت خطا استفاده می‌کنیم؛ زیرا این عملیات به دلیل درخواست‌های شبکه‌ای ممکن است با خطا مواجه شود.

برای مثال، فرض کنید که سرور از دسترس خارج شده یا درخواست ما به درستی ارسال نشده است. در چنین شرایطی، باید اجرای برنامه را متوقف کنیم تا از کرش کردن آن جلوگیری نماییم. سینتکس این کار به شکل زیر خواهد بود:

interface Employee {
    id: number
    employee_name: string
    employee_salary: number
    employee_age: number
    profile_image: string
}

const fetchEmployees = async (): Promise<Array<Employee> | string> => {
    const api = 'http://dummy.restapiexample.com/api/v1/employees'
    try {
        const response = await fetch(api)
        const { data } = await response.json()
        return data
    } catch (error) {
        if (error) {
            return error.message
        }
    }
}

ما این تابع را به عنوان یک تابع async تعریف کرده‌ایم. مقدار بازگشتی را به گونه‌ای در نظر گرفته‌ایم که یا یک آرایه از کارمندان باشد یا یک رشته‌ای از پیام‌های خطا. بنابراین، تایپ promise در اینجا Promise<Array<Employee> | string> خواهد بود.

درون بلاک try کدهایی قرار می‌گیرند که انتظار داریم در شرایط عادی و بدون خطا اجرا شوند. در مقابل، بلاک catch هر گونه خطایی که رخ دهد را دریافت و مدیریت می‌کند. در چنین حالتی، فقط مقدار ویژگی message از آبجکت error را return می‌کنیم.

نکته جالب اینجاست که هر خطایی که درون بلاک try رخ دهد، بلافاصله throw شده و توسط بلاک catch گرفته می‌شود. اگر این خطا مدیریت نشود، ممکن است باعث بروز خطاهای سخت برای دیباگ شده یا حتی کل برنامه را متوقف نماید.

مدیریت خطا با توابع Higher-Order

در حالی که استفاده از بلاک‌های سنتی try/catch برای مدیریت خطاها در سطح لوکال کارآمد است، اما اگر بیش از حد مورد استفاده قرار بگیرد، می‌تواند کد را شلوغ کرده و منطق اصلی برنامه را تحت تأثیر قرار دهد. در چنین شرایطی، استفاده از توابع Higher-Order راه‌حل بهتری خواهد بود.

تابع Higher-Order تابعی است که یک یا چند تابع دیگر را به عنوان آرگومان دریافت می‌کند یا یک تابع را return کند. در زمینه مدیریت خطاها، می‌توانیم از یک تابع Higher-Order برای wrapping یک تابع async استفاده کنیم تا خطاهای احتمالی را مدیریت کند. به این ترتیب، منطق try/catch از هسته اصلی برنامه جدا می‌شود و کد خواناتر و تمیزتر خواهد بود.

ایده اصلی این است که یک تابع Wrapper ایجاد کنیم که یک تابع async/await را همراه با آرگومان‌های لازم دریافت کند. درون این Wrapper یک بلاک try/catch پیاده‌سازی می‌کنیم تا مدیریت خطاها را به صورت مرکزی انجام دهد. این کار باعث می‌شود که کد ما خواناتر و نگه‌داری آن آسان‌تر شود.

برای درک بهتر این موضوع، مثال دریافت اطلاعات کارمندان را بررسی می‌کنیم:

// Async function to fetch employee data
async function fetchEmployees(apiUrl: string): Promise<Employee[]> {
    const response = await fetch(apiUrl);
    const data = await response.json();
    return data;
}

// Wrapped version of fetchEmployees using the higher-order function
const safeFetchEmployees = (url: string) => handleAsyncErrors(fetchEmployees, url);

// Example API URL
const api = 'http://dummy.restapiexample.com/api/v1/employees';

// Using the wrapped function to fetch employees
safeFetchEmployees(api)
    .then(data => {
        if (data) {
            console.log("Fetched employee data:", data);
        } else {
            console.log("Failed to fetch employee data.");
        }
    })
    .catch(err => {
        // This catch block might be redundant, depending on your error handling strategy within the higher-order function
        console.error("Error in safeFetchEmployees:", err);
    });

در این مثال، تابع safeFetchEmployees از تابع Higher-Order handleAsyncErrors برای wrap کردن تابع fetchEmployees اصلی استفاده می‌کند.

این روش به طور خودکار خطاهایی را که ممکن است در طول API call رخ دهد کنترل می‌کند، آن‌ها را ثبت کرده و برای نشان دادن وضعیت خطا، null را برمی‌گرداند. سپس مصرف‌کننده safeFetchEmployees می‌تواند بررسی کند که آیا مقدار بازگشتی تهی است یا خیر، تا تعیین کند که آیا عملیات موفقیت‌آمیز بوده یا خطایی رخ داده است.

اجرای همزمان با Promise.all

همان‌طور که قبلاً به آن اشاره کردیم، گاهی اوقات نیاز داریم که promiseها به صورت همزمان اجرا شوند.

بیایید یک مثال از API کارمندان را بررسی کنیم. فرض کنید ابتدا باید همه کارمندان را دریافت کنیم، سپس نام آن‌ها را استخراج کرده و در نهایت یک ایمیل از نامشان تولید نماییم. بدیهی است که باید این توابع را هم به صورت متوالی و هم به طور همزمان اجرا کنیم تا یکی باعث متوقف شدن دیگری نشود.

در این شرایط، می‌توانیم از Promise.all استفاده کنیم. طبق گفته Mozilla «Promise.all معمولاً زمانی استفاده می‌شود که چندین عملیات asynchronous  به طور همزمان اجرا شده‌اند و ما می‌خواهیم منتظر بمانیم تا همه‌ی آن‌ها تکمیل شوند».

شبه‌کد اجرای همزمان

  1. دریافت تمام کاربران: /employee
  2. منتظر ماندن برای دریافت اطلاعات همه‌ی کاربران، استخراج id از هر کاربر، و دریافت اطلاعات هر کاربر: /employee/{id}
  3. تولید ایمیل برای هر کاربر بر اساس نام کاربری
const baseApi = 'https://reqres.in/api/users?page=1'
const userApi = 'https://reqres.in/api/user'

const fetchAllEmployees = async (url: string): Promise<Employee[]> => {
    const response = await fetch(url)
    const { data } = await response.json()
    return data
}

const fetchEmployee = async (url: string, id: number): Promise<Record<string, string>> => {
    const response = await fetch(`${url}/${id}`)
    const { data } = await response.json()
    return data
}
const generateEmail = (name: string): string => {
    return `${name.split(' ').join('.')}@company.com`
}

const runAsyncFunctions = async () => {
    try {
        const employees = await fetchAllEmployees(baseApi)
        Promise.all(
            employees.map(async user => {
                const userName = await fetchEmployee(userApi, user.id)
                const emails = generateEmail(userName.name)
                return emails
            })
        )
    } catch (error) {
        console.log(error)
    }
}
runAsyncFunctions()

در کد بالا:

اهمیت await در async

نکته‌ی کلیدی این است که داخل تابع async، کدها به ترتیب و خط‌به‌خط اجرا می‌شوند. اگر قبل از تکمیل یک promise، سعی کنیم داده‌ی آن را پردازش کنیم، دچار خطا خواهیم شد. در fetchEmployee نیز همین مسئله صادق است؛ تنها اطلاعات یک کارمند را دریافت کرده و پردازش می‌کنیم.

مدیریت خطا

اگر خطایی رخ دهد، این خطا از promise reject شده به Promise.all منتقل می‌شود و در نهایت به یک استثنا تبدیل می‌گردد که در catch مدیریت می‌شود.

مدیریت successeهای جزئی با Promise.allSettled

Promise.all زمانی مفید است که نیاز داشته باشیم همه‌ promiseها successe شوند، اما در دنیای واقعی، ممکن است برخی عملیات‌ها موفق و برخی دیگر ناموفق باشند. به عنوان مثال، در سیستم مدیریت کارمندان، ممکن است بخواهیم چندین رکورد کارمندان را به‌روزرسانی کنیم، اما برخی به‌روزرسانی‌ها به دلیل خطاهای اعتبارسنجی یا مشکلات شبکه‌ای ناموفق باشند.

در این شرایط، Promise.allSettled بسیار کاربرد دارد. برخلاف Promise.all که در صورت شکست یک promise کل عملیات را متوقف می‌کند، Promise.allSettled تا زمانی که همه promiseها اجرا شوند، منتظر می‌ماند و نتیجه هر promise را (موفق یا ناموفق) گزارش می‌دهد. به عنوان مثال سیستم مدیریت کارکنان را با این روش به‌روزرسانی می‌کنیم:

interface UpdateResult {
    id: number;
    success: boolean;
    message: string;
}

const updateEmployee = async (employee: Employee): Promise<UpdateResult> => {
    const api = `${userApi}/${employee.id}`;
    try {
        const response = await fetch(api, {
            method: 'PUT',
            body: JSON.stringify(employee),
            headers: {
                'Content-Type': 'application/json'
            }
        });
        const data = await response.json();
        return {
            id: employee.id,
            success: true,
            message: 'Update successful'
        };
    } catch (error) {
        return {
            id: employee.id,
            success: false,
            message: error instanceof Error ? error.message : 'Update failed'
        };
    }
};

const bulkUpdateEmployees = async (employees: Employee[]) => {
    const updatePromises = employees.map(emp => updateEmployee(emp));

    const results = await Promise.allSettled(updatePromises);

    // Process results and generate a report
    const summary = results.reduce((acc, result, index) => {
        if (result.status === 'fulfilled') {
            acc.successful.push(result.value);
        } else {
            acc.failed.push({
                id: employees[index].id,
                error: result.reason
            });
        }
        return acc;
    }, {
        successful: [] as UpdateResult[],
        failed: [] as Array<{id: number; error: any}>
    });

    return summary;
};

Promise.allSettled را می‌توانیم مانند یک مدیر پروژه در نظر بگیریم که چندین تسک را پیگیری می‌کند. برخلاف Promise.all که در صورت شکست یک تسک کل فرآیند را متوقف می‌کند، این مدیر به کارهای دیگر ادامه داده و در پایان گزارشی از تمام تسک‌ها (موفق و ناموفق) ارائه می‌دهد.

کاربردهای این روش عبارتند از:

کار با جریان‌های داده با استفاده از for await…of

گاهی نیاز داریم حجم زیادی از داده‌ها را که به صورت دسته‌ای دریافت می‌شوند، پردازش کنیم. فرض کنید در حال export کردن داده‌های کارمندان یک سیستم سازمانی بزرگ هستیم؛ احتمالاً هزاران رکورد داریم که برای جلوگیری از اشغال بیش‌ازحد حافظه، در چندین بخش دریافت می‌شوند.

حلقه for await...of برای این سناریو ایده‌آل است. این حلقه به ما کمک می‌کند تا داده‌های asynchronous را به صورت تک‌تک پردازش کنیم و کدی بهینه و خوانا داشته باشیم. به عنوان مثال در سیستم مدیریت کارکنان داریم:

interface PaginatedResponse<T> {
  data: T[];
  nextPage?: string;
}

async function* fetchAllPages<T>(
  initialUrl: string,
  fetchPage: (url: string) => Promise<PaginatedResponse<T>>
): AsyncIterableIterator<T> {
  let currentUrl = initialUrl;
  while (currentUrl) {
    const response = await fetchPage(currentUrl);

    for (const item of response.data) {
      yield item;
    }
    currentUrl = response.nextPage || '';
  }
}

// Usage with type safety
async function processAllEmployee() {
  const fetchPage = async (url: string): Promise<PaginatedResponse<Employee>> => {
    const response = await fetch(url);
    return response.json();
  };
  try {
    for await (const employee of fetchAllPages('/api/employees', fetchPage)) {
      // Process each employee as they come in
      console.log(`Processing employee: ${employee.employee_name}`);
      await updateEmployeeAnalytics(employee);
    }
  } catch (error) {
    console.error('Failed to process employees:', error);
  }
}
function updateEmployeeAnalytics(employee: Employee) { /** custom logic */}

می‌توانیم for await...of را مانند یک نوار نقاله در کارخانه در نظر بگیریم. به جای اینکه صبر کنیم تا همه محصولات تولید شوند و سپس بسته‌بندی را شروع کنیم، هر محصول را به محض ورود پردازش می‌کنیم.

مزایای این روش عبارتند از:

ترکیب async/await با توابع Higher-Order

ترکیب توابع Higher-Order با async/await، الگوهای قدرتمندی برای مدیریت عملیات‌های asynchronous ایجاد می‌کند.

در سیستم مدیریت کارمندان، معمولاً باید آرایه‌ای از داده‌ها را به صورت asynchronous پردازش کنیم. متدهای آرایه‌ای مانند map، filter و reduce هنگام استفاده از async/await چالش‌هایی دارند که باید به درستی مدیریت شوند. به عنوان مثال:

// Async filter: Keep only active employees
async function filterActiveEmployees(employees: Employee[]) {
    const checkResults = await Promise.all(
        employees.map(async (employee) => {
            const status = await checkEmployeeStatus(employee.id);
            return { employee, isActive: status === 'active' };
        })
    );

    return checkResults
        .filter(result => result.isActive)
        .map(result => result.employee);
}

// Async reduce: Calculate total department salary
async function calculateDepartmentSalary(employeeIds: number[]) {
    return await employeeIds.reduce(async (promisedTotal, id) => {
        const total = await promisedTotal;
        const employee = await fetchEmployeeDetails(id);
        return total + employee.salary;
    }, Promise.resolve(0)); // Initial value must be a Promise
}

نکات مهمی که هنگام کار با async/await در تایپ اسکریپت و توابع آرایه‌ای باید به آن‌ها توجه داشته باشیم عبارت است از:

توابع Higher-Order سفارشی

گاهی نیاز است توابع کمکی خاصی برای عملیات‌های انجام شده روی داده‌های asynchronous ایجاد کنیم. می‌توانیم توابع Higher-Orderای بسازیم که عملکردهای اضافی مانند کش کردن را به عملیات‌های async اضافه کنند.

به‌عنوان مثال، کد زیر از تابع Higher-Order withCache برای افزودن قابلیت کش به یک تابع async که داده‌ها را بر اساس id دریافت می‌کند، استفاده می‌نماید. اگر همان id چندین بار در مدت پنج ثانیه درخواست شود، مقدار کش شده return می‌شود تا از درخواست‌های غیرضروری به شبکه جلوگیری شود.

// Higher-order function for caching async results
function withCache<T>(
    asyncFn: (id: number) => Promise<T>,
    ttlMs: number = 5000
) {
    const cache = new Map<number, { data: T; timestamp: number }>();

    return async (id: number): Promise<T> => {
        const cached = cache.get(id);
        const now = Date.now();

        if (cached && now - cached.timestamp < ttlMs) {
            return cached.data;
        }

        const data = await asyncFn(id);
        cache.set(id, { data, timestamp: now });
        return data;
    };
}

// Usage example
const cachedFetchEmployee = withCache(async (id: number) => {
    const response = await fetch(`${baseApi}/employee/${id}`);
    return response.json();
});

تایپ Awaited

Awaited یک تایپ کمکی است که عملکرد await را در توابع async مدل‌سازی می‌کند. این تایپ مقدار resolve یک promise را استخراج می‌کند و هرگونه لایه‌های تودرتوی promise را حذف می‌نماید.

نحوه‌ استفاده از Awaited

Awaited به ما کمک می‌کند تایپ مقداری را که پس از await دریافت می‌کنیم، مشخص نماییم. این کار باعث می‌شود که تایپ اسکریپت متوجه شود پس از await دیگر با یک promise سروکار نداریم، بلکه با داده‌ واقعی مواجه هستیم.

سینتکس بیسیک آن به صورت زیر می‌باشد:

type MyPromise = Promise<string>;
type AwaitedType = Awaited<MyPromise>; // AwaitedType will be 'string'

تفاوت Awaited و then

Awaited دقیقاً مدل then در promiseها را شبیه‌سازی نمی‌کند، اما هنگام استفاده از then در توابع async می‌تواند مفید باشد. اگر از await درون یک callback مربوط به then استفاده کنیم، Awaited به تعیین تایپ مقدار استخراج شده کمک می‌کند و نیاز به type annotation اضافی را کاهش می‌دهد.

جمع‌بندی

async و await در تایپ اسکریپت به ما این امکان را می‌دهند که کد asynchronous را به صورتی بنویسیم که مانند کد synchronous به نظر برسد. این ویژگی، خوانایی و درک کد را بسیار بهبود می‌بخشد.

مفاهیم کلیدی که هنگام کار با async/await در تایپ اسکریپت باید به خاطر داشته باشیم عبارتند از:

با تسلط بر این مفاهیم، می‌توانیم عملیات‌های asynchronous را در تایپ اسکریپت به‌صورت کارآمد و خوانا مدیریت نماییم.