Skip to Content
TypeScript Intermediate: Types That Survive Real Projects
Intermediate
0 likes
Courses
TypeScript Intermediate: Types That Survive Real Projects

Generic Types for Lists, Results and Caches

Put a type parameter on a type to describe a page of items, a success or error result and a cache, and give it a default when one fits. After this, you write a shape once and fill in the contents where you use it.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already write types like Page<T> or Result<T> and use a default type parameter, skip to "A cache that remembers its value type", then continue.

The last lesson put a type parameter on a function. You can put one on a type too. This is where generics earn their keep in real code, because the same shape keeps showing up with different contents: a page of users and a page of products, a successful result and a failed one, a cache of names and a cache of prices. Write the shape once, and fill in the contents when you use it.

A list with paging

type Page<T> = {
  items: T[];
  total: number;
  page: number;
};

type User = { id: number; name: string };
type Product = { sku: string; price: number };

const users: Page<User> = {
  items: [{ id: 1, name: "Ana" }],
  total: 1,
  page: 1,
};

const products: Page<Product> = {
  items: [{ sku: "PEN-1", price: 12 }],
  total: 1,
  page: 1,
};

console.log(users.items[0].name, products.items[0].price);
Ana 12

Page<User> is the same shape as Page<Product> with items filled in. Without the type parameter you would copy the shape twice and fix both when a field changes. Or worse, you would only fix one.

type Page<T> = { items: T[]; total: number; page: number };
type User = { id: number; name: string };

const users: Page<User> = {
  items: [{ id: 1, nme: "Ana" }],
  total: 1,
  page: 1,
};
page_error.ts(5,20): error TS2561: Object literal may only specify known properties, but 'nme' does not exist in type 'User'. Did you mean to write 'name'?

The typo is caught inside the nested items, not just on the outside of the object.

A result that is either a value or an error

Functions that can fail often return a small object instead of throwing. A generic type holds both cases.

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function parseAge(text: string): Result<number> {
  const n = Number(text);
  if (Number.isNaN(n)) {
    return { ok: false, error: "not a number" };
  }
  return { ok: true, value: n };
}

const result = parseAge("42");
console.log(result.value);
result.ts(14,20): error TS2339: Property 'value' does not exist on type 'Result<number>'.
  Property 'value' does not exist on type '{ ok: false; error: string; }'.

Before you check ok, the compiler does not know which case you have, so value is not safe to read. Check it first.

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function parseAge(text: string): Result<number> {
  const n = Number(text);
  if (Number.isNaN(n)) {
    return { ok: false, error: "not a number" };
  }
  return { ok: true, value: n };
}

for (const text of ["42", "forty"]) {
  const result = parseAge(text);
  if (result.ok) {
    console.log(result.value.toFixed(1));
  } else {
    console.log(result.error);
  }
}
42.0
not a number

Inside the if, result.value is a number. In the else, only error exists. That pattern has a name, discriminated union, and the last lesson of this course goes deeper.

A cache that remembers its value type

function createCache<V>() {
  const store = new Map<string, V>();
  return {
    get(key: string): V | undefined {
      return store.get(key);
    },
    set(key: string, value: V): void {
      store.set(key, value);
    },
  };
}

const profiles = createCache<{ name: string }>();
profiles.set("a", { name: "Ana" });
console.log(profiles.get("a")?.name, profiles.get("zz")?.name);
profiles.set("b", { nome: "Ben" });
cache.ts(16,21): error TS2353: Object literal may only specify known properties, and 'nome' does not exist in type '{ name: string; }'.

The first call to createCache<{ name: string }>() decides what the cache holds, and every later get and set is checked against it. A lookup that misses gives back undefined, so the type says so. That is what V | undefined is for.

A default for the type parameter

If most callers use the same type, give the parameter a default with =.

type Box<T = string> = { content: T };

const note: Box = { content: "hello" };
const size: Box<number> = { content: 3 };

console.log(note, size);
{ content: 'hello' } { content: 3 }

Box with no angle brackets means Box<string>. Use this sparingly. A default hides what the type parameter is, and a reader has to look up the declaration to know.

⚠ Warning: Do not name a type Cache, Response, Request or Event in a plain script file. Those names already exist as browser globals in TypeScript's default type libraries, and the compiler reports a duplicate identifier. Put export {} in the file to make it a module, or pick another name.

Common mistakes

  • Forgetting the angle brackets when you use the type. Page alone is an error for a type that requires its parameter.
  • Putting any as the default so nothing ever complains.
  • Making the type generic over things that never change. If every Page has the same total, only items needs the T.
  • Adding a second and third type parameter before the first has proven useful.

💡 Tip: Say the type out loud with the word "of": a page of users, a result of number, a cache of profiles. If the sentence sounds natural, the generic is probably right.

If you skip this, you will copy the same shape for every kind of data and fix typos in five places. How many types in your project are the same shape with a different word in the middle?

About

Put a type parameter on a type to describe a page of items, a success or error result and a cache, and give it a default when one fits. After this, you write a shape once and fill in the contents where you use it.

Duration
5 min
Last update
10/08/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.