TypeScript 4.9 bêta apporte le nouvel opérateur satisfies

Par:
fredericmazue

mar, 27/09/2022 - 13:17

Microsoft a annoncé a disponibilité de de TypeScript 4.9 bêta, une mouture dont la nouveauté la plus remarquable est l'arrivée d'un nouvel opérateur : satisfies, ainsi que l'amélioration de l'opérateur in.

Operateur satisfies

Microsoft explique que les développeurs TypeScript sont souvent confrontés à un dilemme : s'assurer qu'une expression correspond à un type, tout en souhaitant également conserver le type le plus spécifique de cette expression à des fins d'inférence.

Microsoft donne cet exemple :

// Each property can be a string or an RGB tuple.
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ^^^^ sacré bleu - we've made a typo!
};

// We want to be able to use array methods on 'red'...
const redComponent = palette.red.at(0);

// or string methods on 'green'...
const greenNormalized = palette.green.toUpperCase();

il est écrit bleu dans le code, alors que c'est probablement blue qui aurait du être écrit. Il serait possible de gérer cette faute de frappe bleu en utilisant une annotation de type sur palette, mais en perdant alors les informations sur chaque propriété.

type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette: Record<Colors, string | RGB> = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ The typo is now correctly detected
};

// But we now have an undesirable error here - 'palette.red' "could" be a string.
const redComponent = palette.red.at(0);

Le nouvel opérateur satisfies permet de valider que le type d'une expression correspond à un certain type, sans changer le type résultant de cette expression. A titre d'exemple, on pourrait utiliser satisfies pour valider que toutes les propriétés de palette sont compatibles avec string | number[] :

type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ The typo is now caught!
} satisfies Record<Colors, string | RGB>;

// Both of these methods are still accessible!
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

Operateur in

Microsoft explique qu'en tant que développeurs, nous devons souvent gérer des valeurs qui ne sont pas entièrement connues au moment de l'exécution. En fait, nous ne savons souvent pas si des propriétés existent, si nous obtenons une réponse d'un serveur ou si nous lisons un fichier de configuration. L'opérateur de JavaScript in peut vérifier si une propriété existe sur un objet.

Auparavant, TypeScript nous permettait de restreindre tous les types qui ne répertorient pas explicitement une propriété.

interface RGB {
    red: number;
    green: number;
    blue: number;
}

interface HSV {
    hue: number;
    saturation: number;
    value: number;
}

function setColor(color: RGB | HSV) {
    if ("hue" in color) {
        // 'color' now has the type HSV
    }
    // ...
}

Ici, le type RGB n'a pas listé hue et a été restreint, nous laissant avec le type HSV. Mais qu'en est-il des exemples où aucun type ne mentionne une propriété donnée ? Dans ces cas-là, le langage ne nous a pas beaucoup aidés. Prenons l'exemple suivant en JavaScript :

function tryGetPackageName(context) {
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            return packageJSON.name;
        }
    }

    return undefined;
}

Réécrire cela en TypeScript canonique serait simplement une question de définition et d'utilisation d'un type pour context. Cependant, choisir un type sûr comme unknown pour la propriété packageJSON causerait des problèmes dans les anciennes versions de TypeScript.

En effet, alors que le type de packageJSON est passé de unknown à object, l'opérateur in est strictement limité aux types qui définissaient réellement la propriété vérifiée. En conséquence, le type de packageJSON est resté object.

TypeScript 4.9 rend l'opérateur in un peu plus puissant lors de la réduction des types qui ne répertorient pas du tout la propriété. Au lieu de les laisser tels quels, le langage croisera leurs types avec Record<"property-key-being-checked", unknown>.

Ainsi, dans l'exemple, packageJSON aura son type réduit de unknown à object puis object & Record<"name", unknown>. permet d'accéder directement à packageJSON.name et de le réduire indépendamment

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context): string | undefined {
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            // Just works!
            return packageJSON.name;
        }
    }

    return undefined;
}