TypeScript 5.0 blev officielt frigivet den 16. marts 2023 og er nu tilgængelig for alle. Denne version introducerer mange nye funktioner med det formål at gøre TypeScript mindre, enklere og hurtigere.

Denne nye version moderniserer decorators til klassetilpasning, hvilket gør det muligt at tilpasse klasser og deres medlemmer på en genanvendelig måde. Udviklere kan nu tilføje en const-modifikator til en typeparameterdeklaration, så const-lignende inferencer kan være standard. Den nye version gør også alle enums til union enums, hvilket forenkler kodestrukturen og fremskynder TypeScript-oplevelsen.

I denne artikel vil du udforske de ændringer, der er indført i TypeScript 5.0, og få et dybdegående kig på de nye funktioner og muligheder.

Kom godt i gang med TypeScript 5.0

TypeScript er en officiel compiler, som du kan installere i dit projekt ved hjælp af npm. Hvis du vil begynde at bruge TypeScript 5.0 i dit projekt, kan du køre følgende kommando i projektmappen i dit projekt:

npm install -D typescript

Dette vil installere compileren i mappen node_modules, som du nu kan køre med kommandoen npx tsc.

Du kan også finde instruktioner om brug af den nyere version af TypeScript i Visual Studio Code i denne dokumentation.

Hvad er nyt i TypeScript 5.0?

I denne artikel skal vi undersøge 5 større opdateringer, der er indført i TypeScript. Disse funktioner omfatter:

Moderniserede decorators

Decorators har eksisteret i TypeScript i et stykke tid under et eksperimentelt flag, men den nye version bringer dem op på højde med ECMAScript-forslaget, som nu er i fase 3, hvilket betyder, at det er i en fase, hvor det bliver tilføjet til TypeScript.

Decorators er en måde at tilpasse opførslen af klasser og deres medlemmer på en genanvendelig måde. Hvis du f.eks. har en klasse, der har to metoder, greet og 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 virkelige brugssituationer bør denne klasse have mere komplicerede metoder, der håndterer noget asynkron logik og har sideeffekter, f.eks. hvor du ønsker at indsætte nogle console.log-opkald for at hjælpe med at fejlfinde metoderne.

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();

Dette er et hyppigt forekommende mønster, og det ville være praktisk at have en løsning, der kan anvendes på alle metoder.

Det er her, at decorators kommer ind i billedet. Vi kan definere en funktion ved navn debugMethod, der ser ud som følger:

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 ovenfor tager debugMethod den oprindelige metode (originalMethod) og returnerer en funktion, der gør følgende:

  1. Logger en meddelelse “Method Execution Starts”.
  2. Overfører den oprindelige metode og alle dens argumenter (herunder dette).
  3. Logger en meddelelse “Method Execution Ends”.
  4. Returnerer det, som den oprindelige metode returnerede.

Ved at bruge dekoratorer kan du anvende debugMethod på dine metoder som vist i koden nedenfor:

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();

Dette vil give følgende:

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

Ved definitionen af decorator-funktionen (debugMethod) overføres en anden parameter kaldet context (det er context-objektet – det indeholder nogle nyttige oplysninger om, hvordan den dekorerede metode blev erklæret, og også navnet på metoden). Du kan opdatere din debugMethod for at hente metodens navn fra 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ører din kode, vil output nu indeholde navnet på hver metode, der er dekoreret med debugMethod-decoratoren:

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

Der er mere, end du kan gøre med decoratorer. Du er velkommen til at tjekke den oprindelige pull request for at få flere oplysninger om, hvordan man bruger decorators i TypeScript.

Introduktion af const Type Parameters

Dette er endnu en stor udgivelse, der giver dig et nyt værktøj med generics for at forbedre den inferens, du får, når du kalder funktioner. Som standard, når du deklarerer værdier med const, udleder TypeScript typen og ikke dens bogstavelige værdier:

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

Indtil nu har du for at opnå den ønskede inferens skullet bruge const-assertion ved at tilføje “as const” for at opnå den ønskede inferens:

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

Når du kalder funktioner, er det på samme måde. I koden nedenfor er den udledte type af lande 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 kan ønske en mere specifik type, som en måde at løse før nu har været at tilføje as const assertion:

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

Dette kan være svært at huske og implementere. TypeScript 5.0 introducerer imidlertid en ny funktion, hvor du kan tilføje en const-modifikator til en typeparameterdeklaration, hvilket automatisk vil anvende en const-lignende inferens 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'] });

Ved at bruge const typeparametre kan udviklere udtrykke hensigten mere klart i deres kode. Hvis en variabel skal være konstant og aldrig ændres, sikrer brugen af en const typeparameter, at den aldrig kan ændres ved et uheld.

Du kan se den oprindelige pull request for at få flere oplysninger om, hvordan const-typeparameteren fungerer i TypeScript.

Forbedringer af Enums

Enums i TypeScript er en kraftfuld konstruktion, der giver udviklere mulighed for at definere et sæt navngivne konstanter. I TypeScript 5.0 er der blevet foretaget forbedringer af enums for at gøre dem endnu mere fleksible og nyttige.

Hvis du f.eks. har følgende enum, der er overgivet til en funktion:

enum Color {
    Red,
    Green,
    Blue,
}

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

console.log(getColorName(1));

Før indførelsen af TypeScript 5.0 kunne du sende et forkert niveaunummer, og der ville ikke opstå nogen fejl. Men med indførelsen af TypeScript 5.0 vil det straks give en fejl.

Desuden gør den nye version alle enums til union-enums ved at oprette en unik type for hvert beregnet medlem. Denne forbedring gør det muligt at indsnævre alle enums og henvise til deres medlemmer 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

Forbedringer af ydeevnen i TypeScript 5.0

TypeScript 5.0 indeholder adskillige væsentlige ændringer i kodestruktur, datastrukturer og algoritmiske udvidelser. Dette har været med til at forbedre hele TypeScript-oplevelsen, fra installation til udførelse, hvilket har gjort den hurtigere og mere effektiv.

For eksempel er forskellen mellem pakkestørrelsen i TypeScript 5.0 og 4.9 ganske imponerende.

TypeScript blev for nylig migreret fra namespaces til moduler, hvilket gør det muligt at udnytte moderne buildværktøjer, der kan udføre optimeringer som scope hoisting. Desuden er der ved at fjerne noget forældet kode blevet barberet ca. 26,4 MB af TypeScript 4.9’s 63,8 MB pakkestørrelse.

TypeScript-pakkestørrelse
TypeScript-pakkestørrelse

Her er et par andre interessante gevinster i hastighed og størrelse mellem TypeScript 5.0 og 4.9:

Scenario Tid eller størrelse i forhold til TS 4.9
material-ui build tid 90%
TypeScript-kompilerens opstartstid 89%
Playwright-build tid 88%
TypeScript Compiler-tid til selvopbygning 87%
Byggetid for Outlook Web 82%
VS Code build tid 80%
typescript npm Pakke størrelse 59%

Bundler-opløsning for bedre modulopløsning

Når du skriver en import-anvisning i TypeScript, skal compileren vide, hvad importen henviser til. Det gør den ved hjælp af modulopløsning. Når du f.eks. skriver import { a } from "moduleA", skal compileren kende definitionen af a i moduleA for at kontrollere brugen af den.

I TypeScript 4.7 blev der tilføjet to nye indstillinger for indstillingerne --module og moduleResolution: node16 og nodenext.

Formålet med disse indstillinger var at repræsentere de nøjagtige opslagsregler for ECMAScript-moduler i Node.js mere nøjagtigt. Denne tilstand har dog flere begrænsninger, som ikke håndhæves af andre værktøjer.

I et ECMAScript-modul i Node.js skal enhver relativ import f.eks. indeholde en filudvidelse, for at den fungerer korrekt:

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

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

TypeScript har indført en ny strategi kaldet “moduleResolution bundler”. Denne strategi kan implementeres ved at tilføje følgende kode i afsnittet “compilerOptions” i din TypeScript-konfigurationsfil:

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

Denne nye strategi er velegnet til dem, der bruger moderne bundlere som Vite, esbuild, swc, Webpack, Parcel og andre, der bruger en hybrid opslagsstrategi.

Du kan tjekke den oprindelige pull request og dens implementering for at få flere oplysninger om, hvordan moduleResolution bundler fungerer i TypeScript.

Forældelser

TypeScript 5.0 kommer med en del afskrivninger, herunder krav til kørselstid, lib.d.ts-ændringer og API-brydende ændringer.

  1. Krav til kørselstid: TypeScript er nu rettet mod ECMAScript 2018, og pakken fastsætter en minimumsmotorforventning på 12.20. Derfor skal brugere af Node.js have en minimumsversion på 12.20 eller nyere for at kunne bruge TypeScript 5.0.
  2. lib.d.ts ændringer: Der er sket nogle ændringer i den måde, hvorpå typer til DOM genereres, hvilket kan påvirke eksisterende kode. Især er visse egenskaber blevet konverteret fra taltyper til numeriske bogstavtyper, og egenskaber og metoder til håndtering af hændelseshåndtering for klip, kopiering og indsættelse er blevet flyttet på tværs af grænseflader.
  3. Ændringer i API’et, der bryder med API’et: Nogle unødvendige grænseflader er blevet fjernet, og der er foretaget nogle forbedringer af korrektheden. TypeScript 5.0 er også flyttet til moduler.

TypeScript 5.0 har forældet visse indstillinger og deres tilsvarende værdier, herunder target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, og preserveValueImports, samt prepend i projektreferencer.

Disse konfigurationer vil fortsat være gyldige indtil TypeScript 5.5, men der vil blive udsendt en advarsel for at advare brugere, der stadig bruger dem.

Opsummering

I denne artikel har du lært nogle af de vigtigste funktioner og forbedringer, som TypeScript 5.0 bringer, f.eks. forbedringer af enums, bundler-opløsning og const-typeparametre samt forbedringer af hastighed og størrelse.

Hvis du overvejer TypeScript til dine næste projekter, kan du give Kinsta Applickation Hosting en gratis prøveperiode.

Nu er det din tur! Hvilke funktioner eller forbedringer finder du mest tiltalende i TypeScript 5.0? Er der nogle væsentlige, som vi måske har overset? Lad os vide det i kommentarerne.

Joel Olawanle Kinsta

Joel is a Frontend developer working at Kinsta as a Technical Editor. He is a passionate teacher with love for open source and has written over 200 technical articles majorly around JavaScript and it's frameworks.