TypeScript 5.0 släpptes officiellt den 16 mars år 2023 och är nu tillgängligt för alla. Den här versionen introducerar många nya funktioner med målet att göra TypeScript mindre, enklare och snabbare.

Den nya versionen moderniserar exempelvis dekoratorer för klassanpassning. Som ett resultat så blir det möjligt att anpassa klasser och deras medlemmar på ett återanvändbart sätt. Utvecklare kan nu lägga till en const-modifier till en deklaration av en typparameter. Detta gör att att const-liknande inferenser blir standard. Den nya versionen inkluderar dessutom förbättrade enums. Som ett resultat så förenklas kodstrukturen och TypeScript-upplevelsen snabbas upp.

I den här artikeln så kommer du att utforska de förändringar som infördes i TypeScript 5.0 och få en djupgående titt på dess nya funktioner och möjligheter.

Kom igång med TypeScript 5.0

TypeScript är en officiell kompilator som du kan installera i ditt projekt med hjälp av npm. Om du vill börja använda TypeScript 5.0 i ditt projekt kan du köra följande kommando i projektkatalogen:

npm install -D typescript

Detta kommer sedan att installera kompilatorn i katalogen node_modules, som du nu kan köra med kommandot npx tsc.

Du hittar dessutom instruktioner om hur du använder den nyare versionen av TypeScript i Visual Studio Code i den här dokumentationen.

Vad är nytt i TypeScript 5.0?

I den här artikeln så ska vi utforska 5 viktiga uppdateringar som har införts i TypeScript. Dessa funktioner omfattar exempelvis följande:

Moderniserade dekoratörer

Dekoratörer har funnits i TypeScript ett tag under en experimentell flagga. Den nya versionen gör dem dock uppdaterade med ECMAScript-förslaget, som nu befinner sig i steg 3. Som ett resultat så kan det nu läggas till i TypeScript.

Dekoratörer är ett sätt att anpassa beteendet hos klasser och deras medlemmar på ett återanvändbart sätt. Om du exempelvis har en klass som har två metoder, greet och getAge:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

I verkliga användningsområden så bör den här klassen ha mer komplicerade metoder som hanterar en viss asynkron logik och har sidoeffekter. Där kan du exempelvis lägga in några console.log-anrop för att hjälpa till att felsöka metoderna.

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log('LOG: Method Execution Starts.');
        console.log(`Hello, my name is ${this.name}.`);
        console.log('LOG: Method Execution Ends.');
    }

    getAge() {
        console.log('LOG: Method Execution Starts.');
        console.log(`I am ${this.age} years old.`);
        console.log('Method Execution Ends.');
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

Detta är ett ofta förekommande mönster och det skulle vara praktiskt att ha en lösning som kan tillämpas på varje metod.

Det är här som dekoratörer kommer in i bilden. Vi kan definiera en funktion med namnet debugMethod som ser ut på följande sätt:

function debugMethod(originalMethod: any, context: any) {
    function replacementMethod(this: any, ...args: any[]) {
        console.log('Method Execution Starts.');
        const result = originalMethod.call(this, ...args);
        console.log('Method Execution Ends.');
        return result;
    }
    return replacementMethod;
}

I koden ovan så tar debugMethod den ursprungliga metoden (originalMethod) och returnerar sedan en funktion som gör följande:

  1. Loggar meddelandet ”Method Execution Starts”.
  2. Överlämnar den ursprungliga metoden och alla dess argument (inklusive detta).
  3. Loggar meddelandet ”Method Execution Ends”.
  4. Återger det som den ursprungliga metoden returnerade.

Genom att använda dekoratörer så kan du exempelvis tillämpa debugMethod på dina metoder enligt koden nedan:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    @debugMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
    @debugMethod
    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

Detta kommer sedan att ge följande resultat:

LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.

När man definierar dekoratör-funktionen (debugMethod) så skickas en andra parameter som heter context (det är kontext-objektet – och innehåller en del användbar information om hur den dekorerade metoden deklarerades och även metodens namn). Du kan uppdatera din debugMethod för att få metodnamnet från context-objektet:

function debugMethod(
    originalMethod: any,
    context: ClassMethodDecoratorContext
) {
    const methodName = String(context.name);
    function replacementMethod(this: any, ...args: any[]) {
        console.log(`'${methodName}' Execution Starts.`);
        const result = originalMethod.call(this, ...args);
        console.log(`'${methodName}' Execution Ends.`);
        return result;
    }
    return replacementMethod;
}

När du kör din kod så kommer utdata nu att innehålla namnet på varje metod som är dekorerad med dekoratören debugMethod:

'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.

Det finns mer att göra med dekoratörer. Kolla gärna in ursprunglig pull request för mer information om hur man använder dekoratörer i TypeScript.

Introduktion av const Type Parameters

Detta är en annan stor utgåva som ger dig ett nytt verktyg med generics för att förbättra den inferens som du får när du anropar funktioner. När du deklarerar värden med const, så drar TypeScript standardmässigt slutsatsen om typen och inte om dess bokstavliga värden:

// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];

Hittills så har du varit tvungen att använda const-försäkran genom att lägga till ”as const”, för att få den önskade slutsatsen:

// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;

När du anropar funktioner så är det likadant. I koden nedan så är den härledda typen länder string[]:

type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });

Du kanske vill ha en mer specifik typ? Då kan du lägga till as const-försäkran:

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);

Detta kan vara svårt att komma ihåg och genomföra. TypeScript 5.0 introducerar dock en ny funktion där du kan lägga till en const-modifier till en typparametardeklaration. Som ett resultat så kommer en const-liknande inferens att implementeras som standard.

type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });

Genom att använda const-typparametrar så kan utvecklare uttrycka sin avsikt tydligare i sin kod. Om en variabel är avsedd att vara konstant och aldrig ändras, säkerställer användningen av en const-typparameter exempelvis att den aldrig kan ändras av misstag.

Du kan läsa ursprunglig pull request för mer information om hur const-typparametern fungerar i TypeScript.

Förbättringar av enums

Enums i TypeScript är en kraftfull konstruktion som exempelvis gör det möjligt för utvecklare att definiera en uppsättning namngivna konstanter. I TypeScript 5.0 så har enums förbättrats för att bli ännu mer flexibla och användbara.

Detta gäller om du exempelvis har följande enum som skickas in i en funktion:

enum Color {
    Red,
    Green,
    Blue,
}

function getColorName(colorLevel: Color) {
    return colorLevel;
}

console.log(getColorName(1));

Innan TypeScript 5.0 infördes så kunde du lämna över fel nivånummer utan att något fel uppstod. Efter införandet av TypeScript 5.0 så kommer det däremot omedelbart att uppstå ett fel.

Dessutom så gör den nya versionen alla enums till union enums genom att skapa en unik typ för varje beräknad medlem. Som ett resultat av denna förbättring så blir det möjligt att begränsa alla enums och referera deras medlemmar som typer:

enum Color {
    Red,
    Purple,
    Orange,
    Green,
    Blue,
    Black,
    White,
}

type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
    return c === Color.Red || c === Color.Green || c === Color.Blue;
}

console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true

Prestandaförbättringar i TypeScript 5.0

TypeScript 5.0 inkluderar många betydande förändringar i kodstruktur, datastrukturer och algoritmiska utvidgningar. Som ett resultat så förbättras hela TypeScript-upplevelsen, från installation till exekvering, vilket gör den snabbare och effektivare.

Skillnaden mellan paketstorleken för TypeScript 5.0 och 4.9 är exempelvis ganska imponerande.

TypeScript migrerades nyligen från namnområden till moduler. Det blir därför möjligt att utnyttja moderna byggverktyg som kan utföra optimeringar som exempelvis scope hoisting. Genom att ta bort en del föråldrad kod så har man dessutom sparat cirka 26,4 MB från TypeScript 4.9’s paketstorlek på 63,8 MB.

TypeScript-paketets storlek
TypeScript-paketets storlek

Här är några fler intressanta vinster i hastighet och storlek mellan TypeScript 5.0 och 4.9:

Scenario Tid eller storlek i förhållande till TS 4.9
Tid för att bygga material-ui 90%
Starttid för TypeScript-kompilatorn 89%
Byggtid för Playwright 88%
Tid för TypeScript-kompilatorn för egenkompilering 87%
Byggtid för Outlook Web 82%
Byggtid för VS Code 80%
typescript npm Paketstorlek 59%

Bundler-upplösning för bättre modulupplösning

När du skriver ett importmeddelande i TypeScript så måste kompilatorn veta vad importen avser. Den gör detta med hjälp av modulupplösning. När du exempelvis skriver import { a } from "moduleA" så måste kompilatorn känna till definitionen av a i moduleA för att kunna kontrollera hur den används.

I TypeScript 4.7 så lades två nya alternativ till för inställningarna --module och moduleResolution: node16 och nodenext.

Syftet med dessa alternativ var att mer exakt representera de exakta uppslagsreglerna för ECMAScript-moduler i Node.js. Det här läget har dock flera begränsningar som inte tillämpas av andra verktyg.

I en ECMAScript-modul i Node.js så måste exempelvis varje relativ import inkludera ett filtillägg för att fungera korrekt:

import * as utils from "./utils"; // Wrong 

import * as utils from "./utils.mjs"; // Correct

TypeScript har infört en ny strategi som kallas ”moduleResolution bundler” Denna strategi kan exempelvis implementeras genom att man lägger till följande kod i avsnittet ”compilerOptions” i din TypeScript-konfigurationsfil:

{
    "compilerOptions": {
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

Strategin är lämplig för dem som använder moderna bundles som Vite, esbuild, swc, Webpack, Parcel och andra som använder en hybrid lookup-strategi.

Du kan kontrollera ursprunglig pull request och dess implementering för mer information om hur moduleResolution-bundler fungerar i TypeScript.

Föråldrade funktioner

TypeScript 5.0 inkluderar en del avskrivningar, inklusive körtidskrav, lib.d.ts-ändringar och API-brottsändringar.

  1. Krav på körtid: TypeScript är nu inriktat på ECMAScript 2018, och paketet anger ett lägsta motorkrav på 12.20. Därför så bör användare av Node.js ha en minsta version av 12.20 eller senare för att kunna använda TypeScript 5.0.
  2. lib.d.ts Ändringar: Det har genomförts vissa ändringar i hur typer för DOM genereras. Som ett resultat så kan befintlig kod påverkas. I synnerhet så har vissa egenskaper konverterats från antal till numeriska bokstavstyper. Egenskaper och metoder för hantering av händelser för klippa, kopiera och klistra in har dessutom flyttats över gränssnitt.
  3. API-brottsändringar: Vissa onödiga gränssnitt har tagits bort och vissa korrekthetsförbättringar har gjorts. TypeScript 5.0 har dessutom flyttats till moduler.

Det har avskrivit vissa inställningar och deras motsvarande värden, inklusive target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues och preserveValueImports, samt prepend i projektreferenser.

Även om dessa konfigurationer kommer att vara giltiga fram till TypeScript 5.5 så kommer en varning att utfärdas för att uppmärksamma användare som fortfarande använder dem.

Sammanfattning

I den här artikeln så har du lärt dig några av de viktigaste funktionerna och förbättringarna som TypeScript 5.0 medför. Detta inkluderar exempelvis förbättringar av enums, bundler-upplösning och const-typparametrar, tillsammans med förbättringar av hastighet och storlek.

Om du funderar på TypeScript för dina nästa projekt så kan du ge Kinsta’s Applikationshosting ett kostnadsfritt försök.

Nu är det din tur! Vilka funktioner eller förbättringar tycker du är mest tilltalande i TypeScript 5.0? Finns det några viktiga saker som vi kanske har förbisett? Låt oss veta i kommentarerna.

Joel Olawanle Kinsta

Joel är en frontend-utvecklare som arbetar på Kinsta som teknisk redaktör. Han är en passionerad lärare med kärlek till öppen källkod och har skrivit över 200 tekniska artiklar främst kring JavaScript och dess ramar.