Microsoft annonce la disponibilité de TypeScript 3.1, qui s'accompagne des redirections de versions
ainsi que des types de tableau et de tuple mappables
Redirection de version pour TypeScript via typesVersions
De nombreux utilisateurs de TypeScript et de JavaScript aiment utiliser les fonctionnalités de pointe des langages et des outils. Cela peut parfois créer une situation difficile où les responsables sont obligés de choisir entre prendre en charge de nouvelles fonctionnalités TypeScript et ne pas avoir de cassure avec les anciennes versions de TypeScript.
Par exemple, si vous maintenez une bibliothèque qui utilise le type unknown de TypeScript 3.0, tous vos utilisateurs utilisant des versions antérieures seront pénalisés. Jusqu’à présent, il n’existait malheureusement pas de moyen de fournir des types pour les versions antérieures à 3.0 de TypeScript tout en fournissant également des types pour les versions 3.0 et ultérieures.
Désormais, quand vous utilisez la résolution du module Node dans TypeScript 3.1, lorsque TypeScript ouvre un fichier package.json pour déterminer quels fichiers doivent être lus, il examine d'abord un nouveau champ appelé typesVersions. Un package.json avec un champ typesVersions peut ressembler à ceci:
1 2 3 4 5 6 7 8
| {
"name": "package-name",
"version": "1.0",
"types": "./index.d.ts",
"typesVersions": {
">=3.1": { "*": ["ts3.1/*"] }
}
} |
Ce package.json demande à TypeScript de vérifier si la version actuelle de TypeScript est en cours d'exécution. S'il s'agit d'une version 3.1 ou ultérieure, il identifie le chemin par lequel le package a été importé et lit à partir du dossier ts3.1 du package. C’est ce que {"*": ["ts3.1 / *"]} signifie - si vous êtes familier avec le mappage des chemins, cela fonctionne exactement comme ça.
Donc, dans l'exemple ci-dessus, si nous importons depuis "package-name", nous essaierons de résoudre [...]/node_modules/package-name/ts3.1/index.d.ts (et d'autres chemins d'accès) lors de l'exécution en TypeScript 3.1. Si nous importons depuis package-name / foo, nous essaierons de rechercher [...]/node_modules/package-name/ts3.1/foo.d.ts et [...]/node_modules/package-name/ts3.1/foo/index.d.ts.
Que se passe-t-il si nous n'exécutons pas TypeScript 3.1 dans cet exemple ? Eh bien, si aucun des champs de typesVersions n'est mis en correspondance, TypeScript revient au champ types, donc ici TypeScript 3.0 et les versions antérieures seront redirigés vers [...]/node_modules/package-name/index.d.ts.
Multiple champs
typesVersions peut prendre en charge plusieurs champs où chaque nom de champ est spécifié par la plage à laquelle correspondre.
1 2 3 4 5 6 7 8 9
| {
"name": "package-name",
"version": "1.0",
"types": "./index.d.ts",
"typesVersions": {
">=3.2": { "*": ["ts3.2/*"] },
">=3.1": { "*": ["ts3.1/*"] }
}
} |
Les plages pouvant se chevaucher, la détermination de la redirection s’applique à l’ordre. Cela signifie que dans l'exemple ci-dessus, même si les appariements > = 3.2 et > = 3.1 prennent en charge TypeScript 3.2 et versions ultérieures, l'inversion de l'ordre peut avoir un comportement totalement différent.
1 2 3 4 5 6 7 8 9 10
| {
"name": "package-name",
"version": "1.0",
"types": "./index.d.ts",
"typesVersions": {
// NOTE: this won't work!
">=3.1": { "*": ["ts3.1/*"] },
">=3.2": { "*": ["ts3.2/*"] }
}
} |
Types de tableau et de tuple mappables
Mapper des valeurs dans une liste est l’un des schémas les plus courants de la programmation. À titre d’exemple, regardons le code JavaScript suivant :
1 2 3
| function stringifyAll(...elements) {
return elements.map(x => String(x));
} |
La fonction stringifyAll prend un nombre quelconque de valeurs, convertit chaque élément en chaîne, place chaque résultat dans un nouveau tableau et renvoie ce tableau. Si nous voulons avoir le type le plus général pour stringifyAll, nous le déclarerons comme suit:
declare function stringifyAll(...elements: unknown[]): Array<string>;
Dans son essence, cette portion de code déclare : « cette chose prend un nombre quelconque d'éléments, et retourne un tableau de chaînes ». Cependant, nous avons perdu un peu d’informations sur les éléments de cette transformation.
Plus précisément, le système de types ne se souvient pas du nombre d’éléments transmis à l’utilisateur, de sorte que notre type de sortie n’a pas non plus de longueur connue. Nous pouvons faire quelque chose comme ça avec des surcharges:
1 2 3 4 5
| declare function stringifyAll(...elements: []): string[];
declare function stringifyAll(...elements: [unknown]): [string];
declare function stringifyAll(...elements: [unknown, unknown]): [string, string];
declare function stringifyAll(...elements: [unknown, unknown, unknown]): [string, string, string];
// ... etc |
Plutôt lourd n’est-ce pas ? Et nous n'avons même pas encore couvert quatre éléments. Vous finissez par créer des cas particuliers de toutes les surcharges possibles et vous vous retrouvez avec ce que Microsoft appelle le problème de la « mort par milliers de surcharges ». Bien sûr, nous pourrions utiliser des types conditionnels au lieu de surcharges, mais vous auriez alors un tas de types conditionnels imbriqués.
Si seulement il y avait un moyen de mapper uniformément chacun des types ici, pas vrai ?
Eh bien, TypeScript a déjà quelque chose qui s’y apparente. TypeScript a un concept appelé type d'objet mappé qui peut générer de nouveaux types à partir de ceux existants. Par exemple, en fonction du type de personne suivant,
1 2 3 4 5
| interface Person {
name: string;
age: number;
isHappy: boolean;
} |
nous pourrions vouloir convertir chaque propriété en une chaîne comme ci-dessus:
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface StringyPerson {
name: string;
age: string;
isHappy: string;
}
function stringifyPerson(p: Person) {
const result = {} as StringyPerson;
for (const prop in p) {
result[prop] = String(p[prop]);
}
return result;
} |
Toutefois, stringifyPerson est assez général. Nous pouvons abstraire l'idée de types Stringify-ier en utilisant un type d'objet mappé sur les propriétés d'un type donné:
1 2 3
| type Stringify<T> = {
[K in keyof T]: string
}; |
Pour ceux qui ne sont pas familiers, voici comment cela se lit : « pour chaque propriété nommée K dans T, produit une nouvelle propriété de ce nom avec le type chaîne ».
Il est maintenant temps de réécrire notre fonction pour l'utiliser:
1 2 3 4 5 6 7 8 9
| function stringifyProps<T>(p: T) {
const result = {} as Stringify<T>;
for (const prop in p) {
result[prop] = String(p[prop]);
}
return result;
}
stringifyProps({ hello: 100, world: true }); // has type `{ hello: string, world: string }` |
On dirait que nous avons ce que nous voulons! Cependant, si nous essayions de changer le type de stringifyAll pour retourner un Stringify:
declare function stringifyAll<T extends unknown[]>(...elements: T): Stringify<T>;
Et puis essayons d’appeler sur un tableau ou un tuple, nous n’obtiendrons que quelque chose de presque utile avant TypeScript 3.1. Jetons un coup d’œil à une ancienne version de TypeScript comme 3.0:
1 2 3 4 5
| let stringyCoordinates = stringifyAll(100, true);
// No errors!
let first: string = stringyCoordinates[0];
let second: string = stringyCoordinates[1]; |
On dirait que nos index de tuple ont été mappés correctement ! Vérifions la longueur maintenant pour nous assurer que tout est correct.
1 2 3
| let len: 2 = stringyCoordinates.length
// ~~~
// Type 'string' is not assignable to type '2'. |
Mince ! Une chaîne ? Eh bien, essayons d’itérer nos coordonnées.
1 2 3
| stringyCoordinates.forEach(x => console.log(x));
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Cannot invoke an expression whose type lacks a call signature. Type 'String' has no compatible call signatures. |
Qu'est-ce qui cause ce message d'erreur grossier ? Eh bien notre type mappé Stringify a non seulement mappé nos membres de tuple, il a également mappé sur les méthodes de Array, ainsi que la propriété length ! Donc, forEach et length ont toutes le même type de chaîne !
Bien que son comportement soit cohérent sur le plan technique, la majorité de l’équipe de TypeScript a estimé que ce cas d'utilisation devrait fonctionner. Plutôt que d'introduire un nouveau concept de mappage sur un tuple, les types d'objet mappés ne font désormais que « faire ce qui est bien » lors d'une itération sur des tuples et des tableaux. Cela signifie que si vous utilisez déjà des types mappés existants tels que Partial ou Required de lib.d.ts, ils fonctionnent automatiquement sur les tuples et les tableaux.
Propriétés sur les déclarations de fonction
En JavaScript, les fonctions ne sont que des objets. Cela signifie que nous pouvons y appliquer des propriétés à notre guise :
1 2 3 4 5 6 7
| export function readFile(path) {
// ...
}
readFile.async = function (path, callback) {
// ...
} |
L’approche traditionnelle de TypeScript à cet égard a été une structure extrêmement polyvalente appelée espaces de noms (ou encore « modules internes » si vous êtes suffisamment âgés pour vous en souvenir). En plus d'organiser le code, les espaces de noms prennent en charge le concept de fusion des valeurs, où vous pouvez ajouter des propriétés aux classes et aux fonctions de manière déclarative:
1 2 3 4 5 6 7 8 9
| export function readFile() {
// ...
}
export namespace readFile {
export function async() {
// ...
}
} |
Bien que peut-être élégant pour leur temps, la construction n’a pas bien vieilli. Les modules ECMAScript sont devenus le format préféré pour organiser le nouveau code dans la communauté TypeScript & JavaScript, et les espaces de noms sont spécifiques à TypeScript. De plus, les espaces de noms ne fusionnent pas avec les déclarations var, let ou const, donc le code suivant (motivé par defaultProps de React):
1 2 3 4 5 6 7
| export const FooComponent => ({ name }) => (
<div>Hello! I am {name}</div>
);
FooComponent.defaultProps = {
name: "(anonymous)",
}; |
ne peut pas être simplement converti en
1 2 3 4 5 6 7 8 9 10
| export const FooComponent => ({ name }) => (
<div>Hello! I am {name}</div>
);
// Doesn't work!
namespace FooComponent {
export const defaultProps = {
name: "(anonymous)",
};
} |
Tout cela peut être frustrant car cela rend la migration vers TypeScript plus difficile.
Compte tenu de tout cela, Microsoft a estimé qu'il serait préférable de rendre TypeScript un peu plus « intelligent » sur ces types de modèles. Dans TypeScript 3.1, pour toute déclaration de fonction ou déclaration de const initialisée avec une fonction, le vérificateur de type analyse la portée contenant pour suivre toutes les propriétés ajoutées. Cela signifie que les deux exemples - à la fois le readFile et les exemples FooComponent - fonctionnent sans modification dans TypeScript 3.1 !
En prime, cette fonctionnalité, associée à la prise en charge de JSX.LibraryManagedAttributes par TypeScript 3.0, facilite considérablement la migration d’une base de code React non typée vers TypeScript, car elle comprend quels attributs sont facultatifs en présence de defaultProps:
1 2 3
| // TypeScript understands that both are valid:
<FooComponent />
<FooComponent name="Nathan" /> |
Source : Microsoft
Voir aussi :
Google s'oriente vers TypeScript et voici pourquoi, selon Evan Martin, un ingénieur de la firme qui travaille sur le langage
Babel : la version 7.0 du transpileur JavaScript est disponible avec le support de TypeScript et bien d'autres nouvelles fonctionnalités
La Release Candidate de TypeScript 3.0 vient de sortir : tour d'horizon des nouveautés de cette version majeure du surensemble typé de JavaScript
TypeScript 2.9 est disponible et intègre plusieurs nouveautés, le langage continue de compléter son système de typage
Partager