Hvis du har skrevet TypeScript en stund, men generics fortsatt føles som noe du
kopierer fra Stack Overflow uten å forstå det fullt ut, er denne artikkelen det som fikser det.
Generics er funksjonen som tar TypeScript fra "JavaScript med typeannotasjoner" til et
genuint ekspressivt typesystem. Når de klikker, ser du dem overalt — og du vil naturlig
nå etter dem i stedet for å defaulte til any når ting blir vanskelig. Kapittelet
om generics i TypeScript-håndboken
dekker hele spesifikasjonen; denne artikkelen fokuserer på mønstrene du faktisk vil bruke i ekte kodebaser.
Problemet med any
Generics eksisterer for å løse et spesifikt problem: du vil at en funksjon eller datastruktur skal
fungere med flere typer, men du vil ikke kaste bort typeinformasjonen i prosessen.
Den naive løsningen er any — og den ser fin ut til du innser hva du har gitt opp:
// 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 runtimeTypeinformasjonen går inn, men den kommer ikke ut. Hva du enn gjør med
result er fullstendig ukontrollert. Det er ikke en TypeScript-kodebase — det er JavaScript
med ekstra trinn. Generics løser dette ved å la deg si: "Jeg vet ikke den eksakte typen ennå, men
hva som enn går inn, bør komme ut med den samme typen."
// 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> deklarerer en typeparameter — tenk på den som en variabel for
typer. TypeScript utleder hva T er fra argumentet du sender inn, så du nesten
aldri trenger å skrive det eksplisitt. identity<string>('hello') fungerer, men det gjør
identity('hello') også — TypeScript finner ut av det.
Generiske funksjoner i praksis
Identity-funksjonen er det kanoniske undervisningseksempelet, men ikke noe du ville skrive i produksjon. Her er typene generiske funksjoner som faktisk dukker opp i ekte kodebaser:
// 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[] ✅Legg merke til hvordan TypeScript automatisk utleder T og K fra hvordan
du kaller funksjonen. Du får full typesikkerhet på returverdien uten å skrive en eneste
eksplisitt typeannotasjon på kallstedet. Det er utbyttet.
Generiske grensesnitt og typer
Det er her generics blir uunnværlige for hverdagsarbeidet. Enhver kodebase som snakker med et API ender opp med å trenge en håndfull generiske omslagstyper. Her er de du vil se (og skrive) hele 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) fra et eksempelsvar. Du
kobler dem deretter inn i de generiske omslags-typene dine.Generiske begrensninger
Noen ganger vil du akseptere flere typer, men ikke absolutt alle typer. Generiske
begrensninger lar deg spesifisere hva en typeparameter må ha. Syntaksen er
T extends SomeType — som betyr "T må være tildelbar til 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' }); // ✅Begrensningen låser ikke T til nøyaktig { id: number } —
det betyr at T må ha minst den formen. Så å sende en fullstendig
UserProfile med ti felt er greit. Dette er strukturell typing i praksis, som er
en av TypeScripts kraftigste og
best dokumenterte
funksjoner.
keyof-begrensningen
Et av de mest nyttige generiske mønstrene i TypeScript standardbibliotek og ekte
kodebaser er å kombinere generics med keyof. Det lar deg skrive funksjoner som
aksepterer et egenskapsnavn fra en objekttype og garanterer at returtypen samsvarer med 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 timeStandard typeparametere
Akkurat som funksjonsparametere kan ha standardverdier, kan typeparametere det også. Dette er nyttig når du har en generisk type som nesten alltid brukes med en spesifikk type, men du vil beholde fleksibiliteten for de tilfellene når den ikke gjø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() });Innebygde generiske verktøytyper
TypeScript leveres med et sett av innebygde verktøytyper som alle er implementert ved hjelp av generics du har lært ovenfor. Å forstå hvordan man bruker dem er en av de mest praktiske TypeScript-ferdighetene du kan ha. Her er kjernesettet med ekte eksempler som viser 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 ✅Hvis du konverterer eksisterende JavaScript til TypeScript og bygger disse typene fra bunnen av, kan verktøyet JS to TypeScript hjelpe deg med å få de initielle typene på plass, slik at du kan begynne å legge verktøytyper oppå dem.
Et realistisk end-to-end-eksempel: Typesikker API-klient
Her er mønsteret jeg alltid bruker når jeg bygger et typesikkert API-lag. Det kombinerer generiske funksjoner, generiske grensesnitt og verktøytyper til noe som gir deg end-to-end typesikkerhet fra fetch-kallet hele veien til komponenten som forbruker dataene:
// 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 ✅Vanlige feil
Noen mønstre å unngå når du er komfortabel med generics:
- Bruke
anyinne i en generic. Hvis du skriverfunction wrap<T>(val: T): any, har du beseiret formålet. Hele poenget er at typen flyter gjennom — å brukeanysom returtype eller inne i implementasjonen betyr at TypeScript ikke kan spore den lenger. - Over-begrensning med spesifikke typer. Å skrive
function process<T extends UserProfile>når du bare trengerT extends { id: number }er for restriktivt. Bruk den minimale begrensningen som får implementasjonen din til å fungere — på den måten forblir funksjonen gjenbrukbar på tvers av ulike typer som tilfeldigvis har samme form. - Nå etter generics når en union ville klare det. Hvis en funksjon tar enten en
stringeller etnumberog logikken er forskjellig for hver, erfunction f(arg: string | number)med en typevakt inni renere enn en generic. Generics skinner når den samme logikken gjelder for alle typevarianter. - For mange typeparametere. Hvis du skriver
<T, U, V, W>, ta et steg tilbake. Det er vanligvis et tegn på at funksjonen gjør for mye, eller at typene kunne uttrykkes som et enkelt grensesnitt. TypeScript-kompilatorens kildekode er en god referanse — selv komplekse hjelpeprogrammer trenger sjelden mer enn 2–3 typeparametere.
Oppsummering
Generics er det punktet der TypeScript slutter å være "annotert JavaScript" og begynner
å være et genuint kraftig typesystem. Kjerneideen er enkel: fang typen som en parameter
slik at den flyter gjennom funksjonen eller grensesnittet ditt uten å miste informasjon. Derfra lar begrensninger
deg begrense hva som er akseptabelt, keyof gir deg typesikker egenskapsaksess, og
de innebygde verktøytypene (Partial, Pick, Omit,
Record) håndterer mønstrene du ellers ville skrive for hånd. Det beste neste steget
er å åpne et ekte prosjekt, finne en funksjon der du brukte any fordi du ikke
kjente en annen måte, og erstatte den med en generic.
TypeScript-håndboken om generics
og
referansen for verktøytyper
er de to sidene som er verdt å bokmerke. Og hvis du bygger grensesnitt fra ekte API-nyttelaster,
kan JSON to TypeScript generere de konkrete typene —
deretter er det bare å pakke dem inn i de generiske verktøytypene dine som
ApiResponse<T> og PaginatedResult<T> for å få full
end-to-end typesikkerhet.