Loops in TypeScript and the Types They Give You

TypeScript has the same loops as JavaScript: for, for...of, for...in, while, do...while and the array method forEach. What it adds is a type for each loop variable. for...of gives the element type, for...in always gives string keys, and forEach gives the element type to its callback, which cannot break early.

The loop variable's type
LoopOverVariable type
for (let i = 0; i < a.length; i++)number[]i: number
for (const x of a)number[]x: number
for (const k in obj){ tea: number; cake: number }k: string
for (const i in a)number[]i: string (not number)
for (const [k, v] of Object.entries(obj)){ tea: number; cake: number }k: string, v: number
for (const [k, v] of map)Map<string, number>k: string, v: number
a.forEach((x) => ...)[string, number] tuplex: string | number

Updated

Pick a scenario to see the code, the type the compiler assigns to the loop variable, and what the loop prints when it runs. Two of them are compile errors on purpose, because those are the loops people most often get stuck on in TypeScript.

What type does the loop variable get?

const scores = [90, 72, 85];
for (const s of scores) {
  console.log(s * 2);
}

Type: s: number

The element type of number[] flows straight into the loop variable.

Output

180
144
170

Loops and async code

Await one at a time with for...of

Each request waits for the previous one. The result array is typed from the function's return type.

async function loadAll(ids: number[]): Promise<string[]> {
  const names: string[] = [];
  for (const id of ids) {
    const res = await fetch(`/api/users/${id}`);
    const user: { name: string } = await res.json();
    names.push(user.name);
  }
  return names;
}

Run them together with Promise.all

map starts every request at once and Promise.all waits for all of them. The order of results matches the order of ids.

async function loadAllAtOnce(ids: number[]): Promise<string[]> {
  return Promise.all(
    ids.map(async (id) => {
      const res = await fetch(`/api/users/${id}`);
      const user: { name: string } = await res.json();
      return user.name;
    }),
  );
}

The loops are JavaScript; the types are the new part

TypeScript does not add any loop syntax. It compiles every loop on this page to the same JavaScript, and the JavaScript loops guide on this site covers what each one does at runtime. Here the focus is the compiler: what type each loop variable gets, and the errors that follow.

Most of the time inference does the work. You rarely annotate a loop variable, and if you feel the need to, it usually means the collection itself needs a better type.

Looping over objects without errors

Objects are the awkward case. for...in and Object.keys both hand you plain strings, and indexing a typed object with a plain string is an error under strict mode. Object.entries avoids that by returning [string, value] pairs, and its value type comes from the object's values.

If the object is a lookup table you loop over often, consider typing it as Record<string, V> or storing it in a Map. Both make the loop variable's type obvious and the error disappears.

Narrowing inside a loop

A loop over a union array, such as (string | number)[], gives a union variable. Narrow it with typeof inside the body, and the compiler tracks the narrower type for the rest of that branch.

Narrowing does not survive across rounds of the loop or into callbacks that run later, so check again where the value is used.

How it works

  1. Choose a scenario from the buttons above the code.
  2. Read the type line to see what the compiler thinks the loop variable is.
  3. Check the output box for what the loop prints, or the exact tsc error when it does not compile.
  4. Copy the code and paste it into your project or the TypeScript playground.

Tips

Frequently asked questions

Why is the key a string in a for...in loop?

for...in visits enumerable string keys, including ones inherited through the prototype chain, and TypeScript cannot rule out extra keys on an object at runtime. Typing the key as keyof T would be a promise the language cannot keep, so it stays string.

How do I fix "Element implicitly has an 'any' type because expression of type 'string' can't be used to index"?

Switch to Object.entries(obj) to get keys and values together, or cast the keys once: for (const k of Object.keys(obj) as (keyof typeof obj)[]). Only cast when you know the object has no extra keys.

Can I break out of forEach in TypeScript?

No. As in JavaScript, forEach stops only if the callback throws. Use for...of with break, or some() and find(), which stop at the first match.

Does for...of work on a Map in TypeScript?

Yes, with a target of ES2015 or later. Each round gives a [key, value] pair typed from the Map's generics. For older targets, turn on downlevelIteration.

What type does a classic for loop counter have?

let i = 0 infers number, and a[i] gives the element type. With noUncheckedIndexedAccess on, a[i] becomes the element type or undefined, which is stricter but closer to the truth.

Related: JavaScript loops compared, TypeScript Map, TypeScript Record type, TypeScript data types, TypeScript vs JavaScript.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap