Functions in TypeScript · Lesson 8 of 17
Function Overloads and Async Functions
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));
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");
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));
}
}
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
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); }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.