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.
-
Generics Without the Fog
-
Generic Functions That Keep the Input Type
-
Generic Types for Lists, Results and Caches
-
-
-
Types That Transform Other Types
-
Utility Types: Partial, Pick, Omit and Record
-
Mapped Types You Can Actually Read
-
-
-
Working With Code You Didn't Write
-
@types Packages and When DefinitelyTyped Is Wrong
-
Typing fetch JSON Without Lying to Yourself
-
-
-
Classes and Modules in TypeScript
-
Classes With public, private and protected
-
Modules, Exports and Avoiding Circular Types
-
-
-
Strictness That Pays Off
-
unknown vs any and Safe Parsing
-
Discriminated Unions for API Responses That Vary
-
-
- Final Check: TypeScript Intermediate
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.
Pagealone is an error for a type that requires its parameter. - Putting
anyas the default so nothing ever complains. - Making the type generic over things that never change. If every
Pagehas the sametotal, onlyitemsneeds theT. - 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
Comments (0)
Sign in to commentNo comments yet.