Generics sound advanced, but the idea is simple: write code once and let it work safely with any type. This guide explains TypeScript generics step by step for beginners, with clear examples and the places you already meet them in Playwright.
๐ What You’ll Learn: what generics are, generic functions and arrow functions, multiple type parameters, constraints with extends and keyof, generic interfaces and classes, utility types like Partial and Omit, and generics in Playwright fixtures and API helpers.
๐งช What Are Generics? (Explained Simply)
Generics let you write code that works with many types while still keeping full type safety. You write the logic once and leave a “blank” for the type. The blank is filled in when the code is used.
A useful analogy is a labelled storage box. The box works the same whether it holds books or shoes, but once you write “Books” on the label, you can only put books in it and you know you will get books out. In TypeScript that label is the type parameter, usually written <T>.
๐ค The Problem Generics Solve
Imagine a function that returns the first item of an array. Without generics you have two bad options:
// Option 1: one function per type (repetitive)
function firstString(items: string[]): string { return items[0]; }
function firstNumber(items: number[]): number { return items[0]; }
// Option 2: use any (no type safety)
function firstAny(items: any[]): any { return items[0]; }
const value = firstAny(['chromium', 'firefox']);
value.toFixed(2); // โ no error in the editor, but crashes at runtime!
โจ Your First Generic Function
Add <T> after the function name. T is a placeholder for “whatever type the caller uses”.
function first<T>(items: T[]): T {
return items[0];
}
const browser = first(['chromium', 'firefox']); // T is string, so browser: string
const score = first([90, 85, 70]); // T is number, so score: number
browser.toUpperCase(); // โ
string method allowed
// browser.toFixed(2); // โ Error: toFixed does not exist on type string
TypeScript usually infers T from the arguments. You can also pass it explicitly: first<string>(['a', 'b']).
๐ก Tip: T is just a convention (for “Type”). Other common names are K (key), V (value) and E (element). Descriptive names like TData are fine too.
โก๏ธ Generic Arrow Functions
Arrow functions use the same idea; the <T> goes before the parameters.
const last = <T>(items: T[]): T => items[items.length - 1];
const wrapInArray = <T>(value: T): T[] => [value];
last(['login', 'checkout', 'logout']); // 'logout'
wrapInArray(42); // [42]
๐ Note: In .tsx files, write <T,> (with a comma) so the compiler does not confuse it with a JSX tag.
๐ข Multiple Type Parameters
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p1 = pair('timeout', 30000); // [string, number]
const p2 = pair(1, true); // [number, boolean]
function mapValues<T, R>(items: T[], fn: (item: T) => R): R[] {
return items.map(fn);
}
const lengths = mapValues(['a', 'abc'], s => s.length); // number[]
๐ง Generic Constraints with extends
Sometimes T should not be “anything”. A constraint says “T must have at least these properties”. Use extends for that.
// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('playwright', 'ts'); // โ
strings have length
longest([1, 2, 3], [1]); // โ
arrays have length
// longest(10, 20); // โ Error: number has no length
A very common constraint is K extends keyof T, which means “K must be one of the property names of T”.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Deepesh', role: 'SDET', years: 8 };
getProperty(user, 'name'); // string
getProperty(user, 'years'); // number
// getProperty(user, 'email'); // โ Error: 'email' is not a key of user
๐ฆ Generic Interfaces and Types
Generics are especially powerful in interfaces. A classic example is a wrapper for API responses where only the data part changes.
interface ApiResponse<T> {
status: number;
message: string;
data: T;
}
interface User { id: number; name: string }
interface Product { sku: string; price: number }
const userRes: ApiResponse<User> = {
status: 200, message: 'OK', data: { id: 1, name: 'Asha' },
};
const productsRes: ApiResponse<Product[]> = {
status: 200, message: 'OK', data: [{ sku: 'LP-01', price: 55000 }],
};
// Default type parameter
type Paginated<T = string> = { page: number; items: T[] };
const tags: Paginated = { page: 1, items: ['smoke', 'regression'] };
๐๏ธ Generic Classes
Classes can be generic too. This simple typed store keeps test data of one type only.
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return [...this.items];
}
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
}
const users = new DataStore<{ email: string; active: boolean }>();
users.add({ email: 'a@test.com', active: true });
// users.add('hello'); // โ Error: string is not the stored type
const active = users.find(u => u.active);
๐งฐ Built-in Generic Utility Types
TypeScript ships with ready-made generics you will use every day:
| Utility | What it does | Example |
|---|---|---|
Array<T> | Array of T | Array<string> |
Promise<T> | Async result of T | Promise<void> |
Partial<T> | All properties optional | Override objects |
Required<T> | All properties required | Final config |
Readonly<T> | All properties read-only | Constant test data |
Pick<T, K> | Keep only some properties | Pick<User, 'name'> |
Omit<T, K> | Remove some properties | Omit<User, 'password'> |
Record<K, V> | Object with keys K and values V | Record<string, number> |
interface User { id: number; name: string; email: string; password: string }
type PublicUser = Omit<User, 'password'>; // no password
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;
const patch: UserUpdate = { email: 'new@test.com' };
๐ญ Generics in Playwright: Real Examples
You already use generics in Playwright, maybe without noticing: every async method returns a Promise<T>, and test.extend<T>() is a generic.
1. A typed API helper
import { APIRequestContext, expect } from '@playwright/test';
export async function getJson<T>(request: APIRequestContext, url: string): Promise<T> {
const response = await request.get(url);
expect(response.ok()).toBeTruthy();
return (await response.json()) as T;
}
// usage in a test
interface Todo { id: number; title: string; completed: boolean }
const todo = await getJson<Todo>(request, 'https://jsonplaceholder.typicode.com/todos/1');
expect(todo.completed).toBe(false); // todo is fully typed
2. Typed custom fixtures with test.extend<T>()
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
type MyFixtures = {
loginPage: LoginPage;
testUser: { username: string; password: string };
};
export const test = base.extend<MyFixtures>({
testUser: async ({}, use) => {
await use({ username: 'standard_user', password: 'secret_sauce' });
},
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
Learn more in Playwright Custom Fixtures with Examples.
3. A generic test data loader
import fs from 'fs';
export function loadJson<T>(path: string): T {
return JSON.parse(fs.readFileSync(path, 'utf-8')) as T;
}
const users = loadJson<{ username: string; password: string }[]>('test-data/users.json');
See the full approach in Read JSON data in Playwright.
โ Best Practices
- Use generics when the same logic works for different types; do not add
<T>when one type is enough. - Let TypeScript infer
Twhenever possible; pass it explicitly when inference cannot know it (for example API responses). - Add constraints (
extends) so the function only accepts types it can really handle. - Prefer generics over
any.anyswitches off type checking; generics keep it on. - Remember that
as Tis a promise you make to the compiler; validate important API responses with assertions.
๐๏ธ Practice Exercises
- Write a generic function
lastItem<T>(arr: T[]): T | undefined. - Write
toMap<T, K extends keyof T>(items: T[], key: K)that returns aMapkeyed by that property. - Create
interface Result<T> { ok: boolean; value?: T; error?: string }and a function that returns it. - Build a generic
Queue<T>class withenqueue,dequeueandsize.
โ FAQ
๐ค What are generics in TypeScript in simple terms?
A way to write a function, interface or class once and use it with any type, while TypeScript still tracks exactly which type is used each time.
๐ค What does <T> mean in TypeScript?
T is a type parameter: a placeholder name for a type that will be provided (or inferred) when the code is used.
๐ค What is the difference between generics and any?
any turns off type checking. Generics keep type checking: if you pass strings in, TypeScript knows you get strings out.
๐ค What does T extends mean in generics?
It is a constraint. T extends { id: number } means T can be any type, as long as it has a numeric id property.
๐ค Where are generics used in Playwright?
In Promise<T> return types, test.extend<Fixtures>() for custom fixtures, and typed helpers such as a getJson<T>() API function.
๐ฏ Next Steps
- ๐ TypeScript with Playwright: put it all together
- ๐ TypeScript Classes
- ๐ TypeScript Interfaces
- ๐ญ Playwright API Automation with Page Object Model
- ๐ญ Playwright with TypeScript Training Notes
๐ฌ Watch the full TypeScript tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in
2 thoughts on “TypeScript Generics Explained for Beginners (with Examples and Playwright Use Cases)”