Hvis du har skrevet TypeScript et stykke tid, men generics stadig føles som noget du
kopierer fra Stack Overflow uden at forstå det fuldt ud, er denne artikel løsningen.
Generics er den funktion, der tager TypeScript fra "JavaScript med typeannotationer" til et
genuint ekspressivt typesystem. Når det klikker, ser du dem overalt — og du vil naturligt
nå efter dem frem for at defaulte til any når tingene bliver vanskelige. Kapitlet
om generics i TypeScript-håndbogen
dækker hele specifikationen; denne artikel fokuserer på de mønstre, du faktisk vil bruge i rigtige kodebaser.
Problemet med any
Generics eksisterer for at løse et specifikt problem: du vil have en funktion eller datastruktur til at
fungere med flere typer, men du vil ikke smide typeinformationen væk i processen.
Den naive løsning er any — og det ser fint ud, indtil du indser, hvad du har givet op:
// 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 runtimeTypeinformationen går ind, men den kommer ikke ud. Hvad du end gør med
result er fuldstændig ukontrolleret. Det er ikke en TypeScript-kodebase — det er JavaScript
med ekstra trin. Generics løser dette ved at lade dig sige: "Jeg kender ikke den eksakte type endnu, men
hvad der end går ind, skal komme ud med den samme type."
// 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 — tænk på det som en variabel for
typer. TypeScript udleder, hvad T er fra det argument, du sender ind, så du næsten
aldrig behøver at skrive det eksplicit. identity<string>('hello') virker, men det gør
identity('hello') også — TypeScript finder ud af det.
Generiske funktioner i praksis
Identity-funktionen er det kanoniske undervisningseksempel, men ikke noget du ville skrive i produktion. Her er de typer generiske funktioner, der faktisk dukker op i rigtige 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[] ✅Læg mærke til, hvordan TypeScript automatisk udleder T og K fra, hvordan
du kalder funktionen. Du får fuld typesikkerhed på returværdien uden at skrive en eneste
eksplicit typeannotation på kaldstedet. Det er udbyttet.
Generiske grænseflader og typer
Det er her generics bliver uundværlige for dagligdagsarbejdet. Enhver kodebase, der taler med et API, ender med at have brug for en håndfuld generiske omslagstyper. Her er dem, 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
sætter dem derefter ind i dine generiske omslag.Generiske begrænsninger
Nogle gange vil du acceptere flere typer, men ikke absolut alle typer. Generiske
begrænsninger lader dig specificere, hvad en typeparameter skal have. Syntaksen er
T extends SomeType — hvilket betyder "T skal kunne tildeles 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' }); // ✅Begrænsningen låser ikke T til præcis { id: number } —
det betyder, at T skal have mindst den form. Så at sende en fuld
UserProfile med ti felter er fint. Det her er strukturel typning i aktion, som er
en af TypeScripts mest kraftfulde og
bedst dokumenterede
funktioner.
keyof-begrænsningen
Et af de mest nyttige generiske mønstre i TypeScript standardbibliotek og rigtige
kodebaser er at kombinere generics med keyof. Det lader dig skrive funktioner, der
accepterer et egenskabsnavn fra en objekttype og garanterer, at returtypen matcher den egenskab.
// 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 typeparametre
Ligesom funktionsparametre kan have standardværdier, kan typeparametre det også. Det er nyttigt, når du har en generisk type, der næsten altid bruges med en specifik type, men du vil beholde fleksibiliteten for de tilfælde, hvor den ikke gør.
// 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() });Indbyggede generiske værktøjstyper
TypeScript leveres med et sæt indbyggede værktøjstyper som alle er implementeret ved hjælp af de generics, du har lært ovenfor. At forstå, hvordan man bruger dem, er en af de mest praktiske TypeScript-færdigheder, du kan have. Her er kernesættet med rigtige eksempler der viser outputtypen:
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 typer fra bunden, kan værktøjet JS to TypeScript hjælpe dig med at få de initiale typer på plads, så du kan begynde at lægge værktøjstyper oven på dem.
Et realistisk end-to-end-eksempel: Typsikker API-klient
Her er det mønster, jeg altid bruger, når jeg bygger et typsikkert API-lag. Det kombinerer generiske funktioner, generiske grænseflader og værktøjstyper til noget, der giver dig end-to-end typesikkerhed fra fetch-kaldet hele vejen til komponenten der forbruger 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 ✅Almindelige fejl
Nogle mønstre at undgå, når du er fortrolig med generics:
- Brug af
anyinde i en generic. Hvis du skriverfunction wrap<T>(val: T): any, har du besejret formålet. Hele pointen er, at typen flyder igennem — at brugeanysom returtype eller inde i implementeringen betyder, at TypeScript ikke kan spore den længere. - Over-begrænsning med specifikke typer. At skrive
function process<T extends UserProfile>når du kun behøverT extends { id: number }er for restriktivt. Brug den minimale begrænsning der får din implementering til at fungere — på den måde forbliver funktionen genanvendelig på tværs af forskellige typer der tilfældigvis har samme form. - At nå efter generics når en union ville klare det. Hvis en funktion tager enten en
stringeller etnumberog logikken er forskellig for hver, erfunction f(arg: string | number)med en typevagt indeni renere end en generic. Generics skinner, når den samme logik gælder for alle typevarianter. - For mange typeparametre. Hvis du skriver
<T, U, V, W>, tag et skridt tilbage. Det er normalt et tegn på, at funktionen gør for meget, eller at typerne kunne udtrykkes som en enkelt grænseflade. TypeScript-compilerens kildekode er en god reference — selv komplekse hjælpeprogrammer har sjældent brug for mere end 2–3 typeparametre.
Opsummering
Generics er det punkt, hvor TypeScript holder op med at være "annoteret JavaScript" og begynder
at være et genuint kraftfuldt typesystem. Kernekonceptet er simpelt: fang typen som en parameter
så den flyder gennem din funktion eller grænseflade uden at miste information. Derfra lader begrænsninger
dig indsnævre det, der er acceptabelt, keyof giver dig typesikker egenskabsadgang, og
de indbyggede værktøjstyper (Partial, Pick, Omit,
Record) håndterer de mønstre, du ellers ville skrive i hånden. Det bedste næste skridt
er at åbne et rigtigt projekt, finde en funktion, hvor du brugte any fordi du ikke
vidste en anden måde, og erstatte den med en generic.
TypeScript-håndbogen om generics
og
referencen for værktøjstyper
er de to sider, der er værd at bogmærke. Og hvis du bygger grænseflader fra rigtige API-nyttelaster,
kan JSON to TypeScript generere de konkrete typer —
derefter er det bare et spørgsmål om at pakke dem ind i dine generiske værktøjstyper som
ApiResponse<T> og PaginatedResult<T> for at få fuld
end-to-end typesikkerhed.