TypeScript 5.0 werd officieel uitgebracht op 16 maart 2023, en is nu voor iedereen te gebruiken. Deze release brengt veel nieuwe mogelijkheden met zich mee met als doel TypeScript kleiner, eenvoudiger en sneller te maken.

Deze nieuwe release moderniseert decorators voor klasse-aanpassingen, waardoor klassen en hun members op een herbruikbare manier kunnen worden aangepast. Ontwikkelaars kunnen nu een const modifier toevoegen aan een type parameter declaratie, waardoor const-achtige inferenties de standaard worden. De nieuwe versie maakt ook alle enums union enums, wat de codestructuur vereenvoudigt en de TypeScript ervaring versnelt.

In dit artikel worden de veranderingen in TypeScript 5.0 besproken, met een diepgaande blik op de nieuwe functies en mogelijkheden.

Aan de slag met TypeScript 5.0

TypeScript is een officiële compiler die je met npm in je project kunt installeren. Als je TypeScript 5.0 in je project wilt gaan gebruiken, kun je het volgende commando uitvoeren in de map van je project:

npm install -D typescript

Dit zal de compiler installeren in de map node_modules, die je nu kunt uitvoeren met het commando npx tsc.

Je kunt in deze documentatie ook instructies vinden over het gebruik van de nieuwere versie van TypeScript in Visual Studio Code.

Wat is er nieuw in TypeScript 5.0?

In dit artikel verkennen we 5 belangrijke updates die in TypeScript zijn geïntroduceerd. Deze features zijn:

Gemoderniseerde decorators

Decorators bestaan al een tijdje in TypeScript onder een experimentele vlag, maar de nieuwe versie brengt ze in overeenstemming met het ECMAScript voorstel, dat nu in stadium 3 is, wat betekent dat het in een stadium is waarin het wordt toegevoegd aan TypeScript.

Decorators zijn een manier om het gedrag van klassen en hun members op een herbruikbare manier aan te passen. Als je bijvoorbeeld een klasse hebt die twee methoden heeft, greet en 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();

In echte gebruiksgevallen zou deze klasse ingewikkelder methoden moeten hebben die wat async logica afhandelen en neveneffecten hebben, e.t.c., waarbij je enkele console.log calls zou willen gebruiken om de methoden te helpen debuggen.

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

Dit is een vaak voorkomend patroon, en het zou handig zijn om een oplossing te hebben voor elke methode.

Dit is waar decorators om de hoek komen kijken. We kunnen een functie definiëren met de naam debugMethod die er als volgt uitziet:

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;
}

In bovenstaande code neemt de debugMethod de oorspronkelijke methode (originalMethod) en geeft een functie terug die het volgende doet:

  1. Logt een bericht “Method Execution Starts”.
  2. Geeft de oorspronkelijke methode en al zijn argumenten door (inclusief dit).
  3. Logt het bericht “Method Execution Ends”.
  4. Geeft terug wat de oorspronkelijke methode teruggaf.

Door decorators te gebruiken, kun je de debugMethod toepassen op je methoden, zoals in onderstaande code:

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

Dit levert het volgende op:

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

Bij het definiëren van de decoratorfunctie (debugMethod) wordt een tweede parameter doorgegeven met de naam context (het is het contextobject – heeft wat nuttige informatie over hoe de gedecoreerde methode is gedeclareerd en ook de naam van de methode). Je kunt je debugMethod bijwerken om de naam van de methode uit het object context te halen:

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;
}

Als je je code uitvoert, zal de uitvoer nu de naam dragen van elke methode die “gedecoreerd” is met de debugMethod decorator:

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

Er is meer mogelijk met decoratoren. Bekijk gerust het oorspronkelijke pull request voor meer informatie over het gebruik van decoratoren in TypeScript.

Introductie van const type parameters

Dit is een andere grote release die je een nieuw hulpmiddel geeft met generics om de inferentie te verbeteren die je krijgt als je functies callt. Standaard, wanneer je waarden declareert met const, leidt TypeScript het type af en niet de letterlijke waarden:

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

Tot nu toe moest je, om de gewenste inferentie te krijgen, de const assertie gebruiken door “as const” toe te voegen:

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

Als je functies callt, is het vergelijkbaar. In de onderstaande code is het afgeleide type van landen 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'] });

Je kunt een specifieker type wensen waarvan een manier om het tot nu toe op te lossen was om de as const assertie toe te voegen:

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

Dit kan lastig zijn om te onthouden en uit te voeren. TypeScript 5.0 introduceert echter een nieuwe mogelijkheid waarbij je een const modifier kunt toevoegen aan een type parameter declaratie, waardoor automatisch een const-achtige inferentie als standaard wordt toegepast.

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'] });

Met const type parameters kunnen developers de bedoeling duidelijker in hun code uitdrukken. Als het de bedoeling is dat een variabele constant is en nooit verandert, zorgt het gebruik van een const type parameter ervoor dat hij nooit per ongeluk kan worden veranderd.

Je kunt het oorspronkelijke pull request bekijken voor meer informatie over hoe de const type parameter werkt in TypeScript.

Verbeteringen aan enums

Enums in TypeScript zijn een krachtige constructie waarmee developers een reeks constanten met naam kunnen definiëren. In TypeScript 5.0 zijn verbeteringen aangebracht aan enums om ze nog flexibeler en bruikbaarder te maken.

Bijvoorbeeld, als je de volgende enum in een functie laat opnemen:

enum Color {
    Red,
    Green,
    Blue,
}

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

console.log(getColorName(1));

Vóór de invoering van TypeScript 5.0 kon je een verkeerd levelnummer doorgeven, en het zou geen fout gooien. Maar met de invoering van TypeScript 5.0 wordt er onmiddellijk een fout gegooid.

Ook maakt de nieuwe versie van alle enums union enums door voor elk berekende member een uniek type te maken. Deze verbetering maakt het mogelijk om alle enums te vernauwen en hun members als types aan te duiden:

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

Prestatieverbeteringen van TypeScript 5.0

TypeScript 5.0 bevat talrijke belangrijke wijzigingen in de codestructuur, gegevensstructuren en algoritmische uitbreidingen. Hierdoor is de hele TypeScript ervaring verbeterd, van installatie tot uitvoering, waardoor het sneller en efficiënter wordt.

Het verschil tussen de pakketgrootte van TypeScript 5.0 en 4.9 is bijvoorbeeld behoorlijk indrukwekkend.

TypeScript is onlangs gemigreerd van namespaces naar modules, waardoor het gebruik kan maken van moderne bouwtools die optimalisaties zoals scope hoisting kunnen uitvoeren. Ook het verwijderen van wat verouderde code heeft ongeveer 26,4 MB van de 63,8 MB van TypeScript 4.9 afgehaald.

TypeScript pakketgrootte
TypeScript pakketgrootte

Hier zijn nog een paar interessante winstpunten in snelheid en omvang tussen TypeScript 5.0 en 4.9:

Scenario Tijd of omvang ten opzichte van TS 4.9
materiaal-ui bouwtijd 90%
TypeScript compiler opstarttijd 89%
Playwright bouwtijd 88%
TypeScript compiler zelfbouwtijd 87%
Outlook Web bouwtijd 82%
VS Code bouwtijd 80%
typescript npm pakketgrootte 59%

Bundler resolutie voor betere module resolutie

Als je in TypeScript een import statement schrijft, moet de compiler weten waar de import naar verwijst. Hij doet dit met behulp van module-resolutie. Als je bijvoorbeeld import { a } from "moduleA" schrijft, moet de compiler de definitie van a in moduleA kennen om het gebruik ervan te controleren.

In TypeScript 4.7 zijn twee nieuwe opties toegevoegd voor de instellingen --module en moduleResolution: node16 en nodenext.

Het doel van deze opties was om de exacte opzoekregels voor ECMAScript modules in Node.js nauwkeuriger weer te geven. Deze modus heeft echter verschillende beperkingen die niet worden afgedwongen door andere gereedschappen.

Bijvoorbeeld, in een ECMAScript module in Node.js moet elke relatieve import een bestandsextensie bevatten om correct te werken:

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

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

TypeScript heeft een nieuwe strategie geïntroduceerd genaamd “moduleResolution bundler.” Deze strategie kan geïmplementeerd worden door de volgende code toe te voegen in de sectie “compilerOptions” van je TypeScript configuratiebestand:

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

Deze nieuwe strategie is geschikt voor degenen die moderne bundlers gebruiken, zoals Vite, esbuild, swc, Webpack, Parcel en andere die een hybride lookup strategie gebruiken.

Je kunt het oorspronkelijke pull request en de implementatie ervan bekijken voor meer informatie over hoe moduleResolution bundler werkt in TypeScript.

Afschrijvingen

TypeScript 5.0 gaat gepaard met een paar afschrijvingen , waaronder runtime eisen, lib.d.ts veranderingen, en API brekende veranderingen.

  1. Runtime eisen: TypeScript richt zich nu op ECMAScript 2018, en het pakket stelt een minimale motorverwachting van 12.20. Daarom moeten gebruikers van Node.js een minimale versie van 12.20 of later hebben om TypeScript 5.0 te kunnen gebruiken.
  2. lib.d.ts veranderingen: Er zijn enkele wijzigingen aangebracht in de manier waarop types voor de DOM worden gegenereerd, die gevolgen kunnen hebben voor bestaande code. In het bijzonder zijn bepaalde properties omgezet van numerieke naar letterlijke types, en properties en methoden voor knippen, kopiëren en plakken zijn over interfaces heen verplaatst.
  3. Breaking API wijzigingen: Enkele onnodige interfaces zijn verwijderd, en er zijn enkele correctheidsverbeteringen aangebracht. TypeScript 5.0 is ook overgegaan op modules.

TypeScript 5.0 heeft bepaalde instellingen en hun bijbehorende waarden afgeschaft, waaronder target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, en preserveValueImports, evenals prepend in projectreferenties.

Hoewel deze configuraties geldig blijven tot TypeScript 5.5, wordt er een waarschuwing gegeven aan gebruikers die ze nog gebruiken.

Samenvatting

In dit artikel heb je enkele van de belangrijkste mogelijkheden en verbeteringen geleerd die TypeScript 5.0 brengt, zoals verbeteringen aan enums, bundler resolutie en const type parameters, samen met verbeteringen aan snelheid en grootte.

Als je TypeScript overweegt voor je volgende projecten, probeer dan eens gratis Kinsta’s Applicatie Hosting.

Nu is het jouw beurt! Welke functies of verbeteringen vind jij het meest aantrekkelijk in TypeScript 5.0? Zijn er belangrijke die we misschien over het hoofd hebben gezien? Laat het ons weten in de comments.

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.