C
TypeScript/Async/Lesson 01

Synchronous vs Asynchronous — TypeScript Protects Even Callback Signatures

25 min·theory
This chapter
1/7
TypeScript

Synchronous vs Asynchronous — TypeScript Protects Even Callback Signatures

💡 Why Should You Learn This? — Sync and Async Are the Same, but the Safety Net Is Different

🎯 The runtime behavior of synchronous and asynchronous code is identical between JS and TS — the difference is 'who guarantees the types of callbacks and their results.'
💼 In JS, there is no way to know what arguments the `cb` in `setTimeout(cb, 1000)` receives. In TS, you can lock down the callback signature with a type.
When you encode what an async function returns (`Promise` or `Promise`) in its signature, call-site code gets full autocompletion.
🔗 The return type of `setTimeout` differs between Node.js and the browser (`NodeJS.Timeout` vs `number`) — TS infers the correct type per environment.
🏢 실무에서는
Event handlers, timers, and API calls all accept callbacks. In JS, writing `e.target.value` inside a callback and accidentally typing `e.target.valeu` is only caught at runtime. TS lets you annotate with `(e: React.ChangeEvent) => void`, giving you IDE autocompletion, typo prevention, and automatic tracking of structural changes all at once.

Sync and Async Behave the Same, but Callback Types Differ

1. Runtime Behavior Is Identical

ts
// Synchronous — executed immediately from top to bottom
console.log('1');
console.log('2');
console.log('3');

// Asynchronous — setTimeout's callback goes to the Task Queue
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Output: 1 → 3 → 2

Whether you use TS or JS, this behavior is identical. TypeScript compiles away the types and runs as JavaScript.

2. The Difference Is Explicit 'Callback and Return Type' Annotations

ts
// JS
function onChange(e) {
  console.log(e.target.value); // e is any — does not catch typos
}

// TS
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // ✅ Autocomplete, red underline on typo
}

3. Pin the Return Type of Async Functions

ts
// JS — The caller doesn't know what fetchUser returns just by looking at the signature
function fetchUser(id) {
  return fetch(`/api/${id}`).then(r => r.json());
}

// TS — It's clear from the signature alone that 'Promise<User> will be returned'
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/${id}`);
  return res.json();
}

4. The Return Value of setTimeout Differs by Environment — One Approach Is Only Safe in One Environment

ts
// ❌ Browser-only — In Node.js, a NodeJS.Timeout object is returned, causing a type error
const id1: number = setTimeout(() => {}, 1000);

// ❌ Node.js-only — In a browser environment (no @types/node), the NodeJS namespace itself doesn't exist, causing a compile error
const id2: NodeJS.Timeout = setTimeout(() => {}, 1000);

// ✅ Safe in both environments — TS automatically infers based on lib/@types/node settings
const id3: ReturnType<typeof setTimeout> = setTimeout(() => {}, 1000);
clearTimeout(id3);

> 💡 When building libraries or general-purpose modules, always use ReturnType<typeof setTimeout>. You can write number or NodeJS.Timeout directly only in app code where the environment is fixed.

💻 🅰️ The JS Way — Unknown Callback Argument Types
// ❌ JS — Cannot express the shape of callbacks/return values with signatures

function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: 'Hong Gil-dong' }); // The caller doesn't know what this callback receives
  }, 500);
}

fetchUser(42, (user) => {
  console.log(user.name);    // ✅ Works if lucky
  console.log(user.naem);    // ❌ Typo — undefined at runtime, late discovery
  console.log(user.email);   // ❌ Non-existent field — undefined at runtime
});

// The return value of setTimeout also has no type
const timerId = setTimeout(() => console.log('tick'), 1000);
clearTimeout(timerId); // number in browser, object in Node — both treated as just 'values'

// Asynchronous function signatures are also poor
async function loadOrders(userId) {
  // You have to read the entire function body to know if the return type is Promise<something>
  const res = await fetch(`/api/orders?u=${userId}`);
  return res.json();
}
💻 🅱️ The TS Way — Pin the Callback Signature and Return Type
// ✅ TS — Types specify the shape of callbacks/return values

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

// Explicit callback signature — (user: User) => void
function fetchUser(id: number, callback: (user: User) => void): void {
  setTimeout(() => {
    callback({ id, name: 'Hong Gil-dong' });
  }, 500);
}

fetchUser(42, (user) => {
  // user's type is inferred as User
  console.log(user.name);  // ✅ Autocomplete
  // console.log(user.naem); // ❌ Compile error — 'naem' does not exist on User
  // console.log(user.email); // ❌ Compile error — email does not exist on User
});

// Timer ID — safe inference regardless of environment
const timerId: ReturnType<typeof setTimeout> = setTimeout(
  () => console.log('tick'),
  1000,
);
clearTimeout(timerId);

// Explicit return type in async function signature
async function loadOrders(userId: number): Promise<Order[]> {
  const res = await fetch(`/api/orders?u=${userId}`);
  return res.json() as Promise<Order[]>;
}

interface Order { id: number; item: string; }

// Caller — IDE knows the return is Order[]
loadOrders(42).then((orders) => {
  orders.forEach((o) => console.log(o.item)); // ✅ Autocomplete
});

💡 💡 Key Differences: JS vs TS (Sync/Async)

1. For functions that accept a callback, embed the callback signature in the parameter type

ts
function onLoad(cb: (data: User[]) => void): void { ... }

2. For async functions, annotate the return type in the signature

ts
async function getUser(id: number): Promise<User> { ... }
// From the signature alone, it's clear that 'Promise<User> is returned'

3. Accept timer IDs as ReturnType<typeof setTimeout>
Browsers return number, Node returns NodeJS.Timeout — this is safe regardless of environment.

4. Don't let async results leak as any
The default return type of res.json() is Promise<any>. The key is to pin types at the boundary — either by casting or narrowing with a validation library like zod.

5. Callback hell has the same answer in both JS and TS: async/await
TypeScript provides even better inference support for async/await. This was already covered in the Promise lesson.

⚡ Try It Yourself — Synchronous vs Asynchronous

An executable version with the types stripped from the 🅱️ TS code above. This confirms that the runtime behavior is identical between JS and TS.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

In TypeScript, what is the correct way to safely receive a timer ID in both browser and Node.js environments?
💡 The browser returns a `number` from `setTimeout`, while Node.js returns a `NodeJS.Timeout` object. By capturing the return value with `ReturnType<typeof setTimeout>`, the type is inferred precisely based on the compilation environment (lib settings and the presence of `@types/node`), allowing the same code to compile safely in both environments.
Synchronous vs. Asynchronous — Protecting Callback Signatures - TypeScript