TypeScript har ett rikt typsystem, men du behöver inte förstå allt för att vara produktiv. Verkligheten är att 90% av arbetet görs av en handfull funktioner du når efter varje dag. Det här är den praktiska referensen — primitiver, unioner, generics, verktygstyper och de begränsningsmönster som får allt att klicka. Ingen abstrakt teori; varje exempel är den typ av kod du skriver i ett verkligt projekt. Om du känner JavaScript väl och har gjort lite TypeScript men vill befästa din mentala modell, är det här för dig. Hela historien finns i TypeScript-handboken — den här artikeln är 80/20-versionen.
Primitiver och literaler
TypeScripts primitiva typer mappar direkt till JavaScripts körtidstyper: string, number, boolean, null, undefined, bigint och symbol. Du kommer att använda de tre första ständigt; resten dyker upp i specifika sammanhang.
// Primitives — the foundation
let userId: number = 42;
let username: string = 'alice';
let isActive: boolean = true;
let deletedAt: Date | null = null; // nullable pattern
let refreshToken: string | undefined; // optional patternDär TypeScript blir intressant är literaltyper. Istället för att bara säga att ett värde är en string, kan du säga att det måste vara en av en specifik uppsättning strängar. Det här är mycket mer användbart än en vanlig string-annotering eftersom TypeScript kan fånga ogiltiga värden vid kompilering:
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;
function navigate(dir: Direction) {
console.log(`Moving ${dir}`);
}
navigate('north'); // ✅
navigate('up'); // ❌ Argument of type '"up"' is not assignable to type 'Direction'
// Combine with string for "known values plus free-form"
type EventName = 'click' | 'focus' | 'blur' | (string & {});(string & {}) i slutet håller autokomplettering fungerande för kända värden medan det fortfarande accepterar vilken sträng som helst. Det är ett vanligt mönster i designsystembibliotek.interface vs type — när ska man använda vad
Det här är frågan varje TypeScript-nybörjare ställer. Det praktiska svaret: använd interface för objektformer — särskilt sådana som representerar offentliga API-kontrakt eller som andra typer kommer att utöka. Använd type för unioner, skärningspunkter, mappade typer och allt som inte är rent en objektform. I praktiken är de i stort sett utbytbara för objektformer — välj en konvention och var konsekvent inom en kodbas.
// interface — object shapes, extensible via extends
interface User {
id: number;
email: string;
createdAt: Date;
}
interface AdminUser extends User {
role: 'admin';
permissions: string[];
}
// type — unions, intersections, aliases for anything
type ID = string | number;
type Nullable<T> = T | null;
// Intersection — combine two shapes (common for mixins / HOC props)
type WithTimestamps = {
createdAt: Date;
updatedAt: Date;
};
type UserRecord = User & WithTimestamps;
// type is required here — interface can't express a union of shapes
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: string; code: number };En meningsfull skillnad: gränssnitt stöder deklarationssammanslagning — du kan deklarera samma gränssnitt två gånger och TypeScript slår samman definitionerna. Det är hur bibliotek utökar globala typer (t.ex. lägger till egenskaper till Window). Typer slås inte samman; att omdeklarera en typ är ett fel.
Union- och skärningstyper
Unionstyper (A | B) säger "det här värdet är antingen A eller B". Skärningstyper (A & B) säger "det här värdet är både A och B samtidigt". Unioner finns överallt i riktig kod — det kraftfullaste mönstret de möjliggör är den diskriminerade unionen, vilket är hur du modellerar data som kan vara i olika tillstånd utan att tillgripa valfria fält överallt.
// Discriminated union — model API response states cleanly
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string; retryable: boolean };
// TypeScript narrows the type inside each branch
function render<T>(state: FetchState<T>) {
switch (state.status) {
case 'idle': return '<p>Not started</p>';
case 'loading': return '<p>Loading...</p>';
case 'success': return `<pre>${JSON.stringify(state.data)}</pre>`;
case 'error': return `<p>Error: ${state.error}</p>`;
}
}
// Intersection — common in React/Angular for composing prop types
type ButtonBaseProps = {
label: string;
disabled?: boolean;
};
type IconButtonProps = ButtonBaseProps & {
icon: string;
iconPosition: 'left' | 'right';
};Det diskriminerade unionsmönstret förlitar sig på en diskriminant — en literalegenskap (här status) som är unik för varje variant. TypeScript använder den egenskapen för att begränsa den fullständiga typen inuti villkorliga grenar, vilket ger dig typsäker åtkomst till variantspecifika fält som data eller error.
Generics — delen som snubblar alla
Det motiverande problemet: du vill ha en funktion som fungerar på flera typer, men att använda any kastar bort all typinformation. Generics löser detta — de är typparametrar, skrivna i vinkelparenteser, som låter en funktion eller ett gränssnitt arbeta över en familj av typer medan full typsäkerhet bibehålls.
// Without generics — any loses all information
function first(arr: any[]): any {
return arr[0];
}
const x = first([1, 2, 3]); // x is 'any' — TypeScript can't help you here
// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined ✅
const s = first(['a', 'b', 'c']); // s: string | undefined ✅Syntaxen <T> deklarerar en typparameter. Du kan namnge den vad som helst — T är bara konventionen för en enda generisk typ. När du anropar funktionen härleder TypeScript T från argumenten, så du behöver sällan skriva det explicit. Generiska gränssnitt är lika användbara för att modellera API-former:
// Generic interface — the API wrapper pattern every codebase has
interface ApiResponse<T> {
data: T;
meta: {
page: number;
totalPages: number;
totalItems: number;
};
}
interface User {
id: number;
name: string;
email: string;
}
// The response type is fully typed — no casting needed
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const res = await fetch('/api/users');
return res.json();
}
const response = await fetchUsers();
response.data[0].email; // ✅ TypeScript knows this is a string
response.meta.totalPages; // ✅ TypeScript knows this is a number
// Constrained generics — T must have an 'id' field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
const user = findById(users, 42); // T inferred as Useras för att få den returtyp du vill ha. Det är de två tecknen på att en generic skulle vara renare.Verktygstyper du faktiskt kommer att använda
TypeScript levereras med en uppsättning inbyggda verktygstyper som omvandlar befintliga typer till nya. De eliminerar behovet av att manuellt duplicera eller justera typdefinitioner. Här är de som dyker upp ständigt i riktiga kodbaser:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Partial<T> — all properties become optional
// Perfect for PATCH/update payloads
type UpdateUserPayload = Partial<User>;
// { id?: number; name?: string; email?: string; ... }
async function updateUser(id: number, payload: Partial<User>) {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(payload)
});
}
// Required<T> — all properties become required (opposite of Partial)
interface Config {
apiUrl?: string;
timeout?: number;
retries?: number;
}
type ResolvedConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number }
// Pick<T, K> — keep only specified properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Use in list views where you only need the display fields
// Omit<T, K> — exclude specified properties
type PublicUser = Omit<User, 'role' | 'createdAt'>;
// Safe to expose in API responses
// Readonly<T> — prevents mutation (great for config and frozen state)
const config: Readonly<ResolvedConfig> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// config.timeout = 10000; // ❌ Cannot assign to 'timeout' because it is a read-only property
// Record<K, V> — typed dictionary
type UserCache = Record<number, User>;
const cache: UserCache = {};
cache[42] = { id: 42, name: 'Alice', email: '[email protected]', role: 'user', createdAt: new Date() };
// Common pattern: mapping string keys to a known value shape
type FeatureFlags = Record<string, { enabled: boolean; rolloutPct: number }>;
// ReturnType<T> — infer the return type of a function
function createSession(userId: number) {
return {
token: crypto.randomUUID(),
userId,
expiresAt: new Date(Date.now() + 86_400_000)
};
}
type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }
// — no need to define the type separately and keep it in syncunknown vs any vs never
Dessa tre typer förvirrar de flesta utvecklare tills de förstår vilket problem var och en löser. any är flyktluckan — den inaktiverar typkontroll helt för det värdet. Det är användbart när man migrerar JavaScript till TypeScript, men överanvändning motverkar syftet med TypeScript. unknown är det typsäkra alternativet: värdet kan vara vad som helst, men du måste begränsa det innan du kan göra något med det. never är en typ som aldrig kan inträffa — botten av typhierarkin.
// any — type checking disabled, use sparingly
function dangerousTransform(input: any) {
return input.toUpperCase(); // TypeScript won't warn even if this crashes at runtime
}
// unknown — safe alternative, forces you to check before using
function safeTransform(input: unknown): string {
if (typeof input === 'string') {
return input.toUpperCase(); // ✅ narrowed to string inside this block
}
if (typeof input === 'number') {
return input.toFixed(2); // ✅ narrowed to number
}
throw new Error(`Cannot transform value of type ${typeof input}`);
}
// Common use: parsing untrusted data (API responses, localStorage, user input)
function parseConfig(raw: unknown): ResolvedConfig {
if (
typeof raw === 'object' &&
raw !== null &&
'apiUrl' in raw &&
typeof (raw as any).apiUrl === 'string'
) {
return raw as ResolvedConfig;
}
throw new Error('Invalid config format');
}
// never — the exhaustive switch pattern
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default:
// If you add a new Shape variant and forget to handle it here,
// TypeScript will error: Type 'NewShape' is not assignable to type 'never'
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}Typbegränsning i praktiken
Typbegränsning är hur TypeScript förfinar en bred typ (string | number, unknown, en diskriminerad union) till en specifik typ inuti ett villkorligt block. TypeScript-begränsningsdokumentationen täcker varje vakt på djupet — här är mönstren du kommer att skriva dagligen.
// typeof — primitive narrowing
function formatValue(val: string | number | boolean): string {
if (typeof val === 'string') return val.trim();
if (typeof val === 'number') return val.toLocaleString();
return val ? 'Yes' : 'No';
// TypeScript knows val must be boolean here
}
// instanceof — class/object narrowing
function handleError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'An unknown error occurred';
}
// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };
function makeNoise(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow(); // TypeScript narrows to Cat
} else {
animal.bark(); // TypeScript narrows to Dog
}
}
// Discriminated union narrowing (most common in component state)
type LoadState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
function renderState<T>(state: LoadState<T>) {
if (state.status === 'success') {
console.log(state.data); // ✅ data is accessible
} else if (state.status === 'error') {
console.error(state.message); // ✅ message is accessible
}
}
// Type predicates — custom type guards
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
// After isUser() returns true, TypeScript knows obj is User
function processPayload(payload: unknown) {
if (isUser(payload)) {
console.log(payload.email); // ✅ fully typed
}
}Operatorn typeof är det mest grundläggande begränsningsverktyget. Kombinerat med instanceof, operatorn in och diskriminantegenskapskontroller kan du hantera nästan vilket begränsningsscenario som helst utan att nå för any eller osäkra casts.
Sammanfattning
TypeScripts typsystem belönar att lära sig grunderna väl snarare än att memorera obscyra funktioner. Literaltyper och diskriminerade unioner eliminerar hela kategorier av körtidsfel. Generics låter dig skriva återanvändbara, typsäkra abstraktioner. Verktygstyper som Partial, Pick, Omit och ReturnType håller dina typer DRY. Och unknown med begränsning ger dig säkerheten hos typer även vid kanterna där data kommer in från omvärlden. Det bästa sättet att befästa allt det här är att experimentera interaktivt — TypeScript Playground låter dig klistra in vilket utdrag som helst och se de härledda typerna direkt, ingen setup behövs. När du arbetar med JSON-data i TypeScript-projekt kan verktygen JSON-formateraren och JSON till TypeScript på den här webbplatsen automatiskt generera gränssnittsdefinitioner från riktiga API-nyttolaster.