LearnFrenzy
Sign in Take a free test

Functions in TypeScript · Lesson 8 of 17

Function Overloads and Async Functions

5 min read Updated Nov 8, 2026 Saurabh Samir

Two more function topics come up constantly in real TypeScript code: overloads, for functions whose return type depends on how they're called, and async functions, which return typed promises. Getting async types and error handling right is essential for any app that talks to an API.

In this lesson

  • Function overloads
  • Typing async functions and Promise<T>
  • Promise.all with tuples
  • Errors in catch are unknown

Function overloads

function parse(value: string): number;                 // overload signatures
function parse(value: string[]): number[];
function parse(value: string | string[]): number | number[] {   // the implementation
  return Array.isArray(value) ? value.map(Number) : Number(value);
}

const one = parse("42");              // type: number
const many = parse(["1", "2", "3"]);  // type: number[]
console.log(one + 1, many.map((n) => n * 10));

Output

43 [ 10, 20, 30 ]

Callers see only the overload signatures, so each call gets a precise return type. The implementation signature must be compatible with all of them and isn't callable directly.

Prefer unions or generics when they're enough

Overloads are worth it when the return type really depends on the argument type. If a single signature with a union parameter (or a generic) describes the function, use that – it's simpler to read and maintain.

Async functions

interface Course {
  id: number;
  title: string;
  lessons: number;
}

const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

async function fetchCourse(id: number): Promise<Course> {     // async always returns a Promise
  await sleep(20);                                              // pretend network delay
  if (id <= 0) throw new Error(`Course ${id} not found`);
  return { id, title: id === 1 ? "TypeScript" : "C++ with STL", lessons: id === 1 ? 20 : 33 };
}

const course = await fetchCourse(1);                          // top-level await (ES module)
console.log(`${course.title}: ${course.lessons} lessons`);

const [ts, cpp] = await Promise.all([fetchCourse(1), fetchCourse(2)]);   // typed tuple of results
console.log(ts.title, "+", cpp.title, "=", ts.lessons + cpp.lessons, "lessons");

Output

TypeScript: 20 lessons
TypeScript + C++ with STL = 53 lessons

An async function's return type is always Promise<T>, and await unwraps it to T. Promise.all with an array of different promises returns a tuple with each result's own type.

Errors are unknown

async function load(id: number): Promise<string> {
  if (id === 404) throw new Error("Not Found");
  if (id === 500) throw "server exploded";          // anything can be thrown in JavaScript
  return `course ${id}`;
}

for (const id of [7, 404, 500]) {
  try {
    console.log(await load(id));
  } catch (err) {                                    // err: unknown under strict mode
    if (err instanceof Error) console.log("Error:", err.message);
    else console.log("Unexpected:", String(err));
  }
}

Output

course 7
Error: Not Found
Unexpected: server exploded

Don't assume err is an Error

JavaScript can throw any value, so strict TypeScript types the catch variable as unknown. Narrow it with instanceof Error before reading .message. Likewise, data from await res.json() is any at run time – validate it (by hand or with a library like Zod) before trusting its type.

Typing a fetch helper

interface User { id: number; name: string }

async function getJson<T>(url: string, isValid: (data: unknown) => data is T): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data: unknown = await res.json();
  if (!isValid(data)) throw new Error("Unexpected response shape");
  return data;                                    // now safely typed as T
}

const isUser = (d: unknown): d is User =>
  typeof d === "object" && d !== null && "id" in d && "name" in d &&
  typeof d.id === "number" && typeof d.name === "string";

// const user = await getJson("/api/users/1", isUser);   // user: User

A generic helper plus a type guard (data is T) is how professional code bridges untyped network data and typed application code. Chapter 5 covers type guards in depth.

Practice

Try it yourself

  1. Write async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> that rejects if p takes longer than ms.

    Show solution
    const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
    
    async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
      let timer: ReturnType<typeof setTimeout> | undefined;
      const timeout = new Promise<never>((_, reject) => {
        timer = setTimeout(() => reject(new Error(`timed out after ${ms} ms`)), ms);
      });
      try {
        return await Promise.race([p, timeout]);
      } finally {
        clearTimeout(timer);
      }
    }
    
    const fast = sleep(10).then(() => "fast data");
    const slow = sleep(200).then(() => "slow data");
    console.log(await withTimeout(fast, 100));
    try {
      await withTimeout(slow, 50);
    } catch (e) {
      console.log(e instanceof Error ? e.message : e);
    }

    Output

    fast data
    timed out after 50 ms

Quick check

What does an async function returning a string have as its return type?

Promise<string>.

What is the type of the catch variable in strict mode?

unknown – narrow it before use.

When are overloads better than a union parameter?

When the return type depends on which kind of argument was passed.

Summary

  • Overloads give precise return types per call shape; prefer unions/generics when they suffice.
  • async functions return Promise<T>; Promise.all returns typed tuples.
  • Catch variables are unknown and JSON is untrusted – narrow and validate.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links