TypeScript2025:VarförvislutadeskrivaJavaScript
I State of JS 2024-undersökningen svarade drygt 78% av deltagarna att de använder TypeScript. npm-statistiken visar över 100 miljoner nedladdningar per vecka. GitHub rankar TypeScript som det tredje mest använda språket på plattformen. Frågan "ska jag använda TypeScript?" är i praktiken besvarad. Det stora flertalet har redan gjort valet.
Men att ett verktyg är populärt betyder inte att det är rätt i varje situation, eller att det saknar kostnader. Den här artikeln handlar om vad TypeScript faktiskt ger dig, vad det kostar, vad som hänt i TS 5.x-serien, och hur du migrerar ett befintligt JavaScript-projekt utan att allt stannar i tre månader.
Var TypeScript står 2025
TypeScript föddes som ett Microsoft-internt projekt 2012. I många år var det mest ett kuriosum, och "Java-folk som vill skriva JavaScript" var en vanlig karikatyr. Det stämde inte särskilt bra då heller, men det är intressant att se hur snabbt bilden skiftade.
Runt 2019–2020 nådde TypeScript en kritisk massa. React-ekosystemet fick bra typstöd, Next.js och andra ramverk började generera TypeScript-projekt som default, och stora open source-bibliotek migrerade sina kodbaser. Angular hade kört TypeScript sedan start, men det var React-världens adoption som tippade balansen.
2025 är läget annorlunda mot bara tre år sedan. De flesta jobbannonserna för frontend och fullstack listar TypeScript som krav, inte som "meriterande". Nya bibliotek publicerar typer som en del av paketet, inte som separata @types/-paket. Verktyg som Vite, Turbopack, Bun och Deno har förstklassigt TypeScript-stöd.
Det som kanske säger mest är att motståndet har tystnat. Tidiga kritiker som DHH (som 2023 drog bort TypeScript ur Turbo) representerar en minoritetsposition. De flesta team som testat TypeScript i ett riktigt projekt går inte tillbaka.
Betyder det att JavaScript är dött? Nej. JavaScript är fortfarande runtime-språket: TypeScript kompileras till JavaScript. Men som authoring-språk, alltså det du faktiskt skriver i din editor, har TypeScript blivit default.
Vad TypeScript löser konkret
Det är lätt att fastna i abstrakta argument om typsystem. Här är tre konkreta situationer där TypeScript gör skillnad.
Refactoring utan panik
Säg att du har en funktion som returnerar ett användarobjekt:
// JavaScript
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
Du vet att den returnerar ett objekt med name, email och role. Det står kanske i en kommentar, eller så vet du det för att du skrev API:t. En dag bestämmer backend-teamet att role byter namn till roles (en array istället för en sträng, för nu stöds multipla roller). Du uppdaterar API:t, men missar tre ställen i frontend-koden som läser user.role. Ingen kompilator klagar. Inga tester fångar det om de inte råkar täcka just de flödena. Buggen når produktion.
Med TypeScript:
interface User {
name: string;
email: string;
roles: string[];
}
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<User>;
}
Nu, om du ändrar role: string till roles: string[] i interfacet, markerar tsc varje ställe i kodbasen som fortfarande refererar till user.role. Du får en lista med exakt de filer och rader som behöver ändras. Det tar fem minuter istället för en dag av buggletande.
Det här skalas. I en kodbas med 200 filer är manuell refactoring opålitligt. I en kodbas med 2000 filer är det omöjligt utan typer.
API-kontrakt som lever i koden
Interfaces och type aliases fungerar som dokumentation som inte kan bli inaktuell. En vanlig frustration i JavaScript-projekt är att README:n säger en sak, JSDoc-kommentarerna säger en annan, och koden gör en tredje. TypeScript tvingar fram överensstämmelse. Om din funktion deklarerar att den tar emot en OrderInput med specifika fält, och du skickar in något annat, får du ett felmeddelande direkt i editorn.
Det här är särskilt värdefullt vid API-gränser: mellan frontend och backend, mellan moduler, mellan ditt bibliotek och dess konsumenter. Union types gör det möjligt att uttrycka "det här kan vara A eller B" på ett sätt som JSDoc inte klarar:
type PaymentStatus =
| { status: "pending" }
| { status: "completed"; transactionId: string }
| { status: "failed"; error: string };
function handlePayment(payment: PaymentStatus) {
switch (payment.status) {
case "completed":
// TypeScript vet att transactionId finns här
console.log(payment.transactionId);
break;
case "failed":
// TypeScript vet att error finns här
console.log(payment.error);
break;
}
}
Det här mönstret, discriminated unions, fångar en hel kategori buggar där du antar att ett fält finns men det bara existerar i vissa tillstånd.
Onboarding och kodbas-navigering
Nya utvecklare i ett team behöver förstå kodbasen. I JavaScript läser du funktioner och gissar vilka typer som flödar genom systemet. Du sätter breakpoints, loggar objekt, frågar kollegor. Med TypeScript kan du högerklicka på vilken variabel som helst i VS Code och se dess typ, hoppa till definitionen, hitta alla användningar. Det ersätter inte bra dokumentation, men det ger en baslinje som alltid är korrekt.
Det låter trivialt, men skillnaden märks mest i medelstora till stora projekt. I ett litet projekt med tre filer spelar det ingen roll. I ett projekt med 50 moduler och fyra utvecklare sparar det timmar varje vecka.
Kostnaden
TypeScript har reella kostnader. Att inte prata om dem vore ohederligt.
Build step-komplexitet
JavaScript körs direkt i webbläsaren och i Node.js. TypeScript behöver ett kompileringssteg. Det innebär ytterligare ett verktyg i din pipeline, fler konfigurationsfiler, och fler saker som kan gå sönder.
I praktiken hanteras detta idag av ramverk. Next.js, Vite, och liknande sköter TypeScript-kompilering åt dig. Men om du bygger något utan ramverk, eller underhåller en äldre Node.js-tjänst, är build-steget ett reellt tillägg av komplexitet.
tsconfig.json
TypeScript-konfigurationen är ökänd. En typisk tsconfig.json har 20–30 alternativ som interagerar med varandra på icke-uppenbara sätt. strict slår på en grupp flaggor. moduleResolution har värden som node, node16, nodenext, och bundler som alla beter sig olika. paths kräver att du också konfigurerar din bundler separat.
Resultatet är att tsconfig.json ofta kopieras från projekt till projekt utan att någon riktigt förstår varje rad. Det fungerar, tills det inte gör det, och då felsöker du en konfigurationsfil istället för din applikation.
Typgymnastik
TypeScript har ett kraftfullt typsystem. Det är Turing-komplett, vilket betyder att du i teorin kan uttrycka vad som helst. I praktiken betyder det att biblioteksförfattare ibland skriver typer som ser ut så här:
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
Det här är en mapped conditional type som rekursivt gör alla fält valfria. Det är användbart, men om du aldrig sett den syntaxen förut är den obegriplig. Och det här är ett relativt enkelt exempel. Kolla typerna i bibliotek som zod, trpc, eller effect om du vill se hur långt det kan gå.
Problemet uppstår när team tror att sofistikerade typer alltid är bättre. Ibland är any rätt val. Ibland är en enkel interface tillräcklig. Överkonstruerade typer har samma underhållskostnad som överkonstruerad applikationskod.
Långsammare prototyping
När du experimenterar och snabbt vill testa en idé kan TypeScript bromsa. Du vill skriva en funktion, inte fundera på om returtypen ska vara Promise<Result<User, ApiError>> eller Promise<User | null>. I den fasen vill du iterera snabbt och lägga till struktur senare.
Det här är en riktig kostnad, men den minskar med erfarenhet. Type inference i moderna TypeScript gör att du ofta inte behöver skriva typer alls, kompilatorn räknar ut dem. Men inlärningskurvan finns, särskilt för koncept som as const, template literal types, och conditional types.
Vad som hänt i TS 5.x
TypeScript 5.x-serien har släppts i hög takt. Här är de viktigaste nyheterna.
TS 5.0: Decorators (mars 2023)
TypeScript fick äntligen stöd för TC39 stage 3-decorators. Det här är den standardiserade versionen, inte den experimentella som Angular och MobX använde via experimentalDecorators.
function logged(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`Calling ${methodName}`);
const result = originalMethod.call(this, ...args);
console.log(`${methodName} returned`);
return result;
}
return replacementMethod;
}
class UserService {
@logged
getUser(id: string) {
return db.users.find(id);
}
}
Den praktiska betydelsen: decorators kan nu användas utan att du låser dig till ett icke-standard-beteende. Ramverk som NestJS och liknande kan börja migrera till den standardiserade versionen.
TS 5.1: Enklare returtyper (juni 2023)
Innan 5.1 krävde TypeScript att funktioner som returnerar undefined explicit deklarerade det. Nu tillåts funktioner utan return-sats att ha returtypen undefined implicit. Det låter smått, men det tog bort en vanlig irritation:
// Före 5.1: Fel om returtypen inte explicit var undefined
function logMessage(msg: string): undefined {
console.log(msg);
}
// Från och med 5.1: Fungerar utan explicit returtyp
function logMessage(msg: string) {
console.log(msg);
}
// Returtypen infereras korrekt som void/undefined
5.1 förbättrade också JSX-stödet genom att tillåta att JSX-element returnerar andra typer än JSX.Element, vilket öppnade dörren för bättre integration med nya React-funktioner.
TS 5.2: Using declarations (augusti 2023)
using är TypeScripts implementation av TC39-förslaget "Explicit Resource Management". Det fungerar som using i C# eller with i Python, och resurser städas upp automatiskt:
function readConfig() {
using file = openFile("config.json");
// file stängs automatiskt när scopet lämnas,
// även om ett undantag kastas
return JSON.parse(file.read());
}
Resursen måste implementera Symbol.dispose (eller Symbol.asyncDispose för asynkrona resurser). Det här är användbart för databasanslutningar, filhantering, och temporära resurser som annars kräver try/finally-block.
TS 5.3: Import attributes (november 2023)
Import attributes (tidigare "import assertions") ger dig möjlighet att specificera metadata vid import:
import config from "./config.json" with { type: "json" };
Det här är viktigt för att runtime-miljöer ska kunna hantera icke-JavaScript-moduler korrekt och säkert. Syntaxen ändrades från assert till with i enlighet med TC39-specifikationen.
TS 5.4: NoInfer (mars 2024)
NoInfer är en utility type som hindrar TypeScript från att inferera en typ från en viss position. Det löser ett specifikt men vanligt problem:
function createFSM<S extends string>(config: {
initial: NoInfer<S>;
states: S[];
}) {
// ...
}
// Nu infereras S bara från states-arrayen, inte från initial
createFSM({
initial: "idle", // Kontrolleras mot states
states: ["idle", "loading", "error"],
});
Utan NoInfer hade TypeScript infererat S som "idle" | "loading" | "error" från hela objektet, inklusive initial. Med NoInfer infereras S bara från states, och initial valideras mot det. Det gör att felaktiga initial-värden fångas.
TS 5.5 och 5.6: Inferred type predicates och mer (2024)
TS 5.5 (juni 2024) introducerade inferred type predicates. TypeScript kan nu automatiskt förstå att en filter-funktion smalnar av typen:
const numbers = [1, null, 3, undefined, 5];
const filtered = numbers.filter((n) => n != null);
// Typen är nu number[], inte (number | null | undefined)[]
// Innan 5.5 behövde du skriva en explicit type predicate
Det här var en av de mest efterfrågade förbättringarna. Tidigare krävdes manuella type predicates (x is number) för att filterfunktioner skulle smalna av typer korrekt.
TS 5.6 (september 2024) lade bland annat till --noUncheckedSideEffectImports som kontrollerar att sidoeffekt-imports (import "./setup") faktiskt pekar på befintliga filer, något som tidigare var helt okontrollerat. Det fångade en kategori tysta fel i projekt som använde sidoeffekt-imports för polyfills och liknande.
TS 5.7 (november 2024)
TS 5.7 fortsatte med inkrementella förbättringar, bland annat bättre kontroll av initialiseringsflöden och förbättrad hantering av --target och modulformat. Varje release i 5.x-serien har följt samma mönster: inga dramatiska omskrivningar, men stadiga förbättringar av type inference, felmeddelanden och prestanda.
Type stripping i Node.js
Parallellt med TypeScript-releaserna händer något intressant i Node.js. Från och med Node.js 22.6 finns flaggan --experimental-strip-types. Den gör precis vad namnet säger: tar bort TypeScript-typannotationer och kör resten som JavaScript, utan fullständig kompilering.
node --experimental-strip-types app.ts
Det här är inte en fullständig TypeScript-kompilator. Det utför ingen typkontroll, och stöder inte TypeScript-specifika features som enums eller namespaces. Men för enklare filer gör det att du kan köra .ts-filer direkt utan build-steg.
Riktningen är tydlig: TypeScript-syntax på väg att bli något som runtime-miljöer förstår nativt. Deno har haft det sedan start. Bun likaså. Att Node.js rör sig åt samma håll är signifikant.
Det pågår också ett TC39-förslag (stage 1) för att lägga till typ-syntax direkt i JavaScript-standarden. Om det går igenom, vilket ligger långt fram, innebär det att TypeScript-liknande typannotationer blir en del av JavaScript-specifikationen, även om de ignoreras vid körning.
När JavaScript räcker
TypeScript är inte alltid svaret. Här är situationer där ren JavaScript kan vara det bättre valet.
Engångsskript. Om du skriver ett Node.js-skript som migrerar data en gång och sedan slängs, behöver du inte typer. Du behöver snabbhet. Skriv det i JavaScript, kör det, arkivera det.
Prototyper med kort livslängd. Om du testar en idé under en hackathon eller bygger en proof-of-concept som ska visas en gång, är TypeScript overhead som inte betalar sig. Skriv först, typa sedan, om projektet överlever.
Enkla serverless-funktioner. En Lambda-funktion som tar emot ett event, gör ett API-anrop, och returnerar ett svar behöver inte nödvändigtvis TypeScript. Funktionen är 30 rader, har en ingång och en utgång, och troligen en utvecklare som underhåller den.
Soloprojekt med kort livslängd. Om du är ensam utvecklare och projektet har en horisont på ett par månader, är TypeScript-overhead svårare att motivera. Det är i team och i kod som lever länge som typerna betalar sig mest.
Gemensamt för de här fallen: liten kodbas, få utvecklare, kort livslängd. Ju mer av de tre du har, desto mindre nytta gör TypeScript. Ju mindre av dem du har (stor kodbas, flera utvecklare, lång livslängd) desto mer värdefullt blir det.
Migreringsstrategi: steg för steg
Att migrera ett befintligt JavaScript-projekt till TypeScript behöver inte vara en big bang. Här är en strategi som fungerar i praktiken.
Steg 1: Installera TypeScript och skapa tsconfig.json
npm install --save-dev typescript
npx tsc --init
Börja med en tillåtande konfiguration:
{
"compilerOptions": {
"target": "ES2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
Nyckeln här är allowJs: true och strict: false. Det betyder att TypeScript-kompilatorn accepterar dina befintliga .js-filer utan klagomål. Ingenting går sönder.
Steg 2: Byt namn på en fil i taget
Välj en fil som inte har för många importer, till exempel en utility-fil, en konfigurationsfil, eller en enkel modul. Byt filändelsen från .js till .ts. Fixa eventuella fel som dyker upp. Gör en commit.
Motstå impulsen att migrera allt samtidigt. En fil per pull request är bättre än hundra filer i en jättebranch som ingen orkar granska.
Steg 3: Börja med API-gränser
Typa inte varje intern variabel först. Börja med gränserna: funktioner som exporteras, API-svar, databasmodeller. Det är där typerna ger mest värde, för det är där buggar uppstår vid missförstånd mellan moduler.
// Börja här: typade gränssnitt för dina API-svar
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
// Sedan här: funktioner som exporteras
export async function getProducts(): Promise<ApiResponse<Product[]>> {
// implementation...
}
Steg 4: Använd @ts-check i filer du inte migrerat ännu
Medan du väntar på att migrera en .js-fil kan du lägga till // @ts-check överst. Det aktiverar TypeScript-kontroll i JavaScript-filen utan att du behöver byta filändelse:
// @ts-check
/** @type {string} */
let userName = "Alice";
userName = 42; // Fel! TypeScript klagar i editorn
Det är ett bra sätt att gradvis öka typkontroll utan att röra build-pipelinen.
Steg 5: Lägg till types för tredjepartsberoenden
Installera @types/-paket för dina beroenden som inte skickar egna typer:
npm install --save-dev @types/node @types/express @types/lodash
Kolla om dina beroenden redan inkluderar typer (det står i package.json under types eller typings). Allt fler paket skickar egna typer, så listan med @types/-paket du behöver krymper för varje år.
Steg 6: Slå på strict gradvis
När huvuddelen av kodbasen är migrerad, börja slå på strict-flaggor en i taget:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Börja med noImplicitAny, det är den flagga som fångar flest buggar. strictNullChecks är näst viktigast men genererar också flest fel i omigrerad kod. Slå på en flagga i taget, fixa felen, commita. Släng inte på "strict": true i ett enda steg om du har en stor kodbas.
Steg 7: Integrera i CI
Lägg till tsc --noEmit i din CI-pipeline. Det kör typkontrollen utan att generera output-filer. Om du redan använder ESLint, byt till typescript-eslint med typed linting för att få regler som förstår dina typer:
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
Med det på plats fångas typfel i PR-granskningen, inte i produktion.
Tidslinje
En rimlig tidslinje för migrering beror på kodbasstorleken. Några riktmärken:
- Litet projekt (< 50 filer): 1–2 veckor med gradvis migrering vid sidan av vanligt arbete.
- Medelstort projekt (50–200 filer): 1–2 månader. Prioritera API-gränser och delade moduler.
- Stort projekt (200+ filer): 3–6 månader. Kräver en plan, tydliga milstolpar, och att teamet är överens om prioriteringsordningen.
Det viktigaste är att migreringen inte blockerar annat arbete. allowJs: true gör att typade och otypade filer kan samexistera. Nya filer skrivs i TypeScript, gamla filer migreras när de ändå behöver ändras. Efter ett halvår har du migrerat de mest berörda delarna utan att ha stannat utvecklingen.
Är TypeScript värt besväret?
TypeScript är inte perfekt. Konfigurationen är onödigt komplex. Avancerade typer kan vara svårlästa. Build-steget lägger till ett lager av indirection. Inlärningskurvan är real, särskilt för generics och conditional types.
Men för de flesta webbprojekt 2025 överväger fördelarna kostnaderna. Refactoring-säkerhet, API-kontrakt, och kodnavigering är inte akademiska argument, det är saker som sparar konkret tid varje vecka i ett aktivt projekt.
TS 5.x-serien har gjort språket bättre utan att göra det mer komplicerat. Type inference har förbättrats så att du skriver färre manuella typer. Nya features som using, decorators, och NoInfer löser specifika problem utan att introducera nya koncept du måste lära dig om du inte behöver dem.
Och riktningen är tydlig: Node.js, Deno, och Bun rör sig alla mot att köra TypeScript-syntax utan kompileringssteg. Det löser inte alla problem (du vill fortfarande ha typkontroll i din editor och CI) men det tar bort det mest uppenbara motargumentet.
Om du startar ett nytt projekt idag: använd TypeScript. Om du har ett befintligt JavaScript-projekt: migrera gradvis med allowJs: true, börja med API-gränser, och slå på strict sist. Det behöver inte vara komplicerat.