Om du har skrivit TypeScript ett tag men generics fortfarande känns som något du
kopierar från Stack Overflow utan att fullt förstå, är den här artikeln det som fixar det.
Generics är funktionen som tar TypeScript från "JavaScript med typannotationer" till ett
genuint expressivt typsystem. När de klickar ser du dem överallt — och du sträcker dig
efter dem naturligt istället för att defaulta till any när saker blir knepigt. Handbokens kapitel
om generics i TypeScript-handboken
täcker hela specifikationen; den här artikeln fokuserar på de mönster du faktiskt kommer att använda i riktiga kodbaser.
Problemet med any
Generics finns för att lösa ett specifikt problem: du vill att en funktion eller datastruktur ska
fungera med flera typer, men du vill inte kasta bort typinformationen i processen.
Den naiva lösningen är any — och det ser bra ut tills du inser vad du har gett upp:
// With any — TypeScript has no idea what comes out
function identity(arg: any): any {
return arg;
}
const result = identity('hello');
// result is typed as 'any' — you've lost the string type
// TypeScript won't catch this:
result.toFixed(2); // no error at compile time, crashes at runtimeTypinformationen går in, men den kommer inte ut. Vad du än gör med
result är helt okontrollerat. Det är inte en TypeScript-kodbas — det är JavaScript
med extra steg. Generics löser detta genom att låta dig säga: "Jag vet inte den exakta typen ännu, men
vad som än går in ska komma ut med samma typ."
// With a generic — T flows through the function
function identity<T>(arg: T): T {
return arg;
}
const result = identity('hello');
// result is typed as 'string' ✅
result.toFixed(2); // ❌ TypeScript catches this: Property 'toFixed' does not exist on type 'string'
const count = identity(42);
// count is typed as 'number' ✅<T> deklarerar en typparameter — tänk på det som en variabel för
typer. TypeScript härleder vad T är från argumentet du skickar in, så du behöver nästan
aldrig skriva det explicit. identity<string>('hello') fungerar, men det gör även
identity('hello') — TypeScript räknar ut det.
Generiska funktioner i praktiken
Identity-funktionen är det kanoniska undervisningsexemplet men inte något du skulle skriva i produktion. Här är de typer av generiska funktioner som faktiskt dyker upp i riktiga kodbaser:
// A type-safe array first/last helper
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
const firstUser = first(users); // UserProfile | undefined
const lastOrder = last(orders); // Order | undefined
const firstNum = first([1, 2, 3]); // number | undefined
// Group array items by a key
function groupBy<T, K extends string | number>(
items: T[],
getKey: (item: T) => K
): Record<K, T[]> {
return items.reduce((acc, item) => {
const key = getKey(item);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {} as Record<K, T[]>);
}
// TypeScript infers T as Order and K as string
const byStatus = groupBy(orders, order => order.status);
// byStatus: Record<string, Order[]>
// byStatus['pending'] is Order[] ✅Lägg märke till hur TypeScript härleder T och K automatiskt från hur
du anropar funktionen. Du får full typsäkerhet på returvärdet utan att skriva en enda
explicit typannotation på anropsplatsen. Det är utdelningen.
Generiska gränssnitt och typer
Det här är där generics blir oumbärliga för vardagsarbete. Varje kodbas som pratar med ett API behöver en handfull generiska omslagstyper. Här är de du kommer att se (och skriva) hela tiden:
// API response wrapper — used across every endpoint
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// Paginated list response
interface PaginatedResult<T> {
items: T[];
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
// Using them with concrete types
interface UserProfile {
id: number;
name: string;
email: string;
avatarUrl: string;
}
interface Order {
id: number;
userId: number;
total: number;
status: 'pending' | 'shipped' | 'delivered';
}
// The return types are fully typed — no casting needed downstream
async function getUser(id: number): Promise<ApiResponse<UserProfile>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function listOrders(page: number): Promise<PaginatedResult<Order>> {
const res = await fetch(`/api/orders?page=${page}`);
return res.json();
}
const response = await getUser(1);
response.data.email; // ✅ string
response.data.avatarUrl; // ✅ string
const result = await listOrders(1);
result.items[0].status; // ✅ 'pending' | 'shipped' | 'delivered'
result.totalPages; // ✅ numberUserProfile, Order) automatiskt från ett exempelsvar. Du
kopplar sedan in dem i dina generiska omslag.Generiska begränsningar
Ibland vill du acceptera flera typer, men inte absolut vilken typ som helst. Generiska
begränsningar låter dig specificera vad en typparameter måste ha. Syntaxen är
T extends SomeType — vilket betyder "T måste vara tilldelningsbar till SomeType":
// Without constraint — TypeScript doesn't know T has an id property
function findById<T>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id); // ❌ Property 'id' does not exist on type 'T'
}
// With constraint — T must have at least an id: number field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id); // ✅
}
// Works with any type that has id: number
const user = findById(users, 42); // UserProfile | undefined
const order = findById(orders, 101); // Order | undefined
// Another common constraint — T must be an object (excludes primitives)
function mergeDefaults<T extends object>(partial: Partial<T>, defaults: T): T {
return { ...defaults, ...partial };
}
// T must have a name: string and email: string
function formatContact<T extends { name: string; email: string }>(contact: T): string {
return `${contact.name} <${contact.email}>`;
}
// Works on any object with those two fields — UserProfile, Employee, whatever
formatContact({ name: 'Alice', email: '[email protected]', role: 'admin' }); // ✅Begränsningen låser inte T till exakt { id: number } —
det betyder att T måste ha åtminstone den formen. Så att skicka en fullständig
UserProfile med tio fält är OK. Det här är strukturell typning i praktiken, vilket är
en av TypeScripts kraftfullaste och
välst dokumenterade
funktioner.
keyof-begränsningen
Ett av de mest användbara generiska mönstren i TypeScript standardbibliotek och riktiga
kodbaser är att kombinera generics med keyof. Det låter dig skriva funktioner som
accepterar ett egenskapsnamn från en objekttyp och garanterar att returtypen matchar den egenskapen.
// keyof T is the union of all keys of T as string literals
// K extends keyof T means K must be one of those keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: UserProfile = {
id: 1,
name: 'Alice',
email: '[email protected]',
avatarUrl: 'https://example.com/avatar.png'
};
const name = getProperty(user, 'name'); // string ✅
const id = getProperty(user, 'id'); // number ✅
const email = getProperty(user, 'email'); // string ✅
getProperty(user, 'password'); // ❌ Argument of type '"password"' is not assignable to
// parameter of type 'keyof UserProfile'
// Real use case: a generic sort function
function sortBy<T>(items: T[], key: keyof T): T[] {
return [...items].sort((a, b) => {
const av = a[key];
const bv = b[key];
return av < bv ? -1 : av > bv ? 1 : 0;
});
}
const byName = sortBy(users, 'name'); // ✅ sorted by name
const byId = sortBy(users, 'id'); // ✅ sorted by id
sortBy(users, 'nonexistent'); // ❌ caught at compile timeStandardtypparametrar
Precis som funktionsparametrar kan ha standardvärden kan typparametrar det också. Det här är användbart när du har en generisk typ som nästan alltid används med en specifik typ, men du vill behålla flexibiliteten för de fall då den inte gör det.
// Default type parameter — T defaults to string if not specified
interface Cache<T = string> {
get(key: string): T | undefined;
set(key: string, value: T, ttlMs?: number): void;
delete(key: string): void;
clear(): void;
}
// Without specifying T — uses the default (string)
declare const stringCache: Cache;
const val = stringCache.get('theme'); // string | undefined
// Specifying a different T
declare const userCache: Cache<UserProfile>;
const user2 = userCache.get('user:42'); // UserProfile | undefined
// Another useful example: an event emitter with a typed payload
interface TypedEvent<TPayload = void> {
subscribe(handler: (payload: TPayload) => void): () => void;
emit(payload: TPayload): void;
}
// Events with no payload — default void keeps the API clean
const appReady: TypedEvent = { /* ... */ };
appReady.emit(); // no argument needed
// Events with a payload
const userLoggedIn: TypedEvent<{ userId: number; timestamp: Date }> = { /* ... */ };
userLoggedIn.emit({ userId: 42, timestamp: new Date() });Inbyggda generiska verktygstyper
TypeScript levereras med en uppsättning inbyggda verktygstyper som alla är implementerade med hjälp av de generics du har lärt dig ovan. Att förstå hur man använder dem är en av de mest praktiska TypeScript-färdigheter du kan ha. Här är kärnuppsättningen med verkliga exempel som visar utdatatypen:
interface UserProfile {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'viewer';
passwordHash: string;
createdAt: Date;
}
// Partial<T> — all fields become optional (great for PATCH payloads)
type UpdateUserPayload = Partial<UserProfile>;
// { id?: number; name?: string; email?: string; role?: ...; ... }
// Required<T> — all fields become required (reverse of Partial)
interface DraftConfig {
apiUrl?: string;
timeout?: number;
maxRetries?: number;
}
type ResolvedConfig = Required<DraftConfig>;
// { apiUrl: string; timeout: number; maxRetries: number }
// Pick<T, K> — keep only the named fields
type UserSummary = Pick<UserProfile, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// Use in list views — only ship what the UI needs
// Omit<T, K> — exclude the named fields
type PublicUserProfile = Omit<UserProfile, 'passwordHash'>;
// { id: number; name: string; email: string; role: ...; createdAt: Date }
// Safe to include in API responses
// Record<K, V> — typed dictionary / map
type PermissionMap = Record<UserProfile['role'], string[]>;
// { admin: string[]; user: string[]; viewer: string[] }
const permissions: PermissionMap = {
admin: ['read', 'write', 'delete', 'admin'],
user: ['read', 'write'],
viewer: ['read']
};
// ReturnType<T> — infer what a function returns (keeps types in sync automatically)
function buildUserSession(user: UserProfile) {
return {
token: crypto.randomUUID(),
userId: user.id,
role: user.role,
expiresAt: new Date(Date.now() + 3_600_000)
};
}
type UserSession = ReturnType<typeof buildUserSession>;
// { token: string; userId: number; role: 'admin' | 'user' | 'viewer'; expiresAt: Date }
// Change buildUserSession and UserSession updates automatically ✅Om du konverterar befintligt JavaScript till TypeScript och bygger dessa typer från grunden kan verktyget JS to TypeScript hjälpa dig att få de initiala typerna på plats så du kan börja lägga verktygstyper ovanpå dem.
Ett realistiskt end-to-end-exempel: Typad API-klient
Här är mönstret jag alltid använder när jag bygger ett typad API-lager. Det kombinerar generiska funktioner, generiska gränssnitt och verktygstyper till något som ger dig end-to-end typsäkerhet från fetch-anropet hela vägen till komponenten som konsumerar data:
// The response envelope — wraps every API response
interface ApiResponse<T> {
data: T;
meta: {
requestId: string;
duration: number;
};
}
// Error envelope — what the API sends on failure
interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
// The generic fetch wrapper
async function fetchApi<T>(
url: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options?.headers },
...options
});
if (!res.ok) {
const err: ApiError = await res.json();
throw new Error(`[${err.code}] ${err.message}`);
}
return res.json();
}
// Typed endpoint functions — T is set at each call site
async function getUser(id: number): Promise<UserProfile> {
const { data } = await fetchApi<UserProfile>(`/api/users/${id}`);
return data;
}
async function listOrders(userId: number, page = 1): Promise<PaginatedResult<Order>> {
const { data } = await fetchApi<PaginatedResult<Order>>(
`/api/users/${userId}/orders?page=${page}`
);
return data;
}
async function createOrder(
payload: Pick<Order, 'userId' | 'total'>
): Promise<Order> {
const { data } = await fetchApi<Order>('/api/orders', {
method: 'POST',
body: JSON.stringify(payload)
});
return data;
}
// Usage — fully typed, no casting anywhere
const user = await getUser(42);
console.log(user.email); // string ✅
const orders = await listOrders(42);
orders.items[0].status; // 'pending' | 'shipped' | 'delivered' ✅
orders.totalPages; // number ✅
const newOrder = await createOrder({ userId: 42, total: 99.99 });
newOrder.id; // number ✅Vanliga misstag
Några mönster att undvika när du är bekväm med generics:
- Använda
anyinuti en generic. Om du skriverfunction wrap<T>(val: T): anyhar du besegrat syftet. Hela poängen är att typen flödar igenom — att användaanysom returtyp eller inuti implementationen innebär att TypeScript inte kan spåra den längre. - Över-begränsa med specifika typer. Att skriva
function process<T extends UserProfile>när du bara behöverT extends { id: number }är för restriktivt. Använd den minimala begränsningen som får din implementering att fungera — på det sättet förblir funktionen återanvändbar för olika typer som råkar ha samma form. - Sträcka sig efter generics när en union duger. Om en funktion tar antingen en
stringeller ettnumberoch logiken är olika för varje, ärfunction f(arg: string | number)med en typguard inuti renare än en generic. Generics lyser när samma logik gäller för alla typvarianter. - För många typparametrar. Om du skriver
<T, U, V, W>, ta ett steg tillbaka. Det är vanligtvis ett tecken på att funktionen gör för mycket, eller att typerna kunde uttryckas som ett enda gränssnitt. Själva TypeScript-kompilatorns källkod är en bra referens — även komplexa verktyg behöver sällan mer än 2–3 typparametrar.
Sammanfattning
Generics är den punkt där TypeScript slutar vara "annoterat JavaScript" och börjar
vara ett genuint kraftfullt typsystem. Kärnidén är enkel: fånga typen som en parameter
så att den flödar genom din funktion eller gränssnitt utan att förlora information. Därifrån låter begränsningar
dig begränsa vad som är acceptabelt, keyof ger dig typsäker egenskapsåtkomst, och
de inbyggda verktytstyperna (Partial, Pick, Omit,
Record) hanterar de mönster du annars skulle skriva för hand. Det bästa nästa steget
är att öppna ett riktigt projekt, hitta en funktion där du använde any för att du inte
visste ett annat sätt, och ersätta den med en generic.
TypeScript-handboken om generics
och
verktygstypreferensen
är de två sidorna värda att bokmärka. Och om du bygger gränssnitt från riktiga API-nyttolaster,
kan JSON to TypeScript generera de konkreta typerna —
sedan är det bara att wrappa dem i dina generiska verktygstyper som
ApiResponse<T> och PaginatedResult<T> för att få fullständig
end-to-end typsäkerhet.