Exploration du système de types
Le service de langage TypeScript
Le service de langage TypeScript, également appelé tsserver, offre diverses fonctionnalités telles que le signalement des erreurs, les diagnostics, la compilation lors de l’enregistrement, le renommage, l’accès à la définition, les listes de complétion, l’aide sur les signatures et bien plus encore. Il est principalement utilisé par les environnements de développement intégrés (IDE) pour assurer la prise en charge d’IntelliSense. Il s’intègre parfaitement à Visual Studio Code et est utilisé par des outils tels que Conquer of Completion (Coc).
Les développeurs peuvent exploiter une API dédiée et créer leurs propres plugins personnalisés pour le service de langage afin d’améliorer l’expérience d’édition de TypeScript. Cela peut être particulièrement utile pour mettre en œuvre des fonctionnalités spécifiques de linting ou activer l’autocomplétion pour un langage de gabarits personnalisé.
« typescript-styled-plugin » est un exemple concret de plugin personnalisé : il signale les erreurs de syntaxe et prend en charge IntelliSense pour les propriétés CSS dans les composants stylisés.
Pour obtenir davantage d’informations et consulter des guides de démarrage rapide, vous pouvez vous référer au wiki officiel de TypeScript sur GitHub : https://github.com/microsoft/TypeScript/wiki/
Typage structurel
TypeScript repose sur un système de types structurel. Cela signifie que la compatibilité et l’équivalence des types sont déterminées par leur structure ou leur définition réelle, plutôt que par leur nom ou leur lieu de déclaration, comme c’est le cas dans les systèmes de types nominaux tels que ceux de C# ou C.
Le système de types structurel de TypeScript a été conçu d’après le fonctionnement du typage canard dynamique de JavaScript lors de l’exécution.
L’exemple suivant est du code TypeScript valide. Comme vous pouvez le constater, « X » et « Y » possèdent le même membre « a », bien qu’ils aient des noms de déclaration distincts. Les types sont déterminés par leurs structures et, dans ce cas, puisque celles-ci sont identiques, ils sont compatibles et valides.
type X = { a: string;};type Y = { a: string;};const x: X = { a: 'a' };const y: Y = x; // ValidRègles fondamentales de comparaison de TypeScript
Le processus de comparaison de TypeScript est récursif et s’applique aux types imbriqués à n’importe quel niveau.
Un type « X » est compatible avec « Y » si « Y » possède au moins les mêmes membres que « X ».
type X = { a: string;};const y = { a: 'A', b: 'B' }; // Valid, as it has at least the same members as Xconst r: X = y;Les paramètres de fonction sont comparés selon leur type, et non selon leur nom :
type X = (a: number) => void;type Y = (a: number) => void;let x: X = (j: number) => undefined;let y: Y = (k: number) => undefined;y = x; // Validx = y; // ValidLes types de retour des fonctions doivent être identiques :
type X = (a: number) => undefined;type Y = (a: number) => number;let x: X = (a: number) => undefined;let y: Y = (a: number) => 1;y = x; // Invalidx = y; // InvalidLe type de retour d’une fonction source doit être un sous-type du type de retour d’une fonction cible :
let x = () => ({ a: 'A' });let y = () => ({ a: 'A', b: 'B' });x = y; // Validy = x; // Invalid member b is missingL’omission de paramètres de fonction est autorisée, car il s’agit d’une pratique courante en JavaScript, par exemple avec « Array.prototype.map() » :
[1, 2, 3].map((element, _index, _array) => element + 'x');Par conséquent, les déclarations de type suivantes sont tout à fait valides :
type X = (a: number) => undefined;type Y = (a: number, b: number) => undefined;let x: X = (a: number) => undefined;let y: Y = (a: number) => undefined; // Missing b parametery = x; // ValidTous les paramètres facultatifs supplémentaires du type source sont valides :
type X = (a: number, b?: number, c?: number) => undefined;type Y = (a: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Validx = y; //ValidTous les paramètres facultatifs du type cible sans paramètres correspondants dans le type source sont valides et ne constituent pas une erreur :
type X = (a: number) => undefined;type Y = (a: number, b?: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Validx = y; // ValidLe paramètre rest est traité comme une série infinie de paramètres facultatifs :
type X = (a: number, ...rest: number[]) => undefined;let x: X = a => undefined; //validLes fonctions avec des surcharges sont valides si la signature de surcharge est compatible avec leur signature d’implémentation :
function x(a: string): void;function x(a: string, b: number): void;function x(a: string, b?: number): void { console.log(a, b);}x('a'); // Validx('a', 1); // Valid
function y(a: string): void; // Invalid, not compatible with implementation signaturefunction y(a: string, b: number): void;function y(a: string, b: number): void { console.log(a, b);}y('a');y('a', 1);La comparaison des paramètres de fonction réussit si les paramètres source et cible peuvent être affectés à des supertypes ou des sous-types (bivariance).
// Supertypeclass X { a: string; constructor(value: string) { this.a = value; }}// Subtypeclass Y extends X {}// Subtypeclass Z extends X {}
type GetA = (x: X) => string;const getA: GetA = x => x.a;
// Bivariance does accept supertypesconsole.log(getA(new X('x'))); // Validconsole.log(getA(new Y('Y'))); // Validconsole.log(getA(new Z('z'))); // ValidLes énumérations sont comparables et compatibles avec les nombres, et inversement, mais la comparaison de valeurs d’énumération provenant de types d’énumération différents est invalide.
enum X { A, B,}enum Y { A, B, C,}const xa: number = X.A; // Validconst ya: Y = 0; // ValidX.A === Y.A; // InvalidLes instances d’une classe sont soumises à une vérification de compatibilité de leurs membres privés et protégés :
class X { public a: string; constructor(value: string) { this.a = value; }}
class Y { private a: string; constructor(value: string) { this.a = value; }}
let x: X = new Y('y'); // InvalidLa comparaison ne tient pas compte des différences dans la hiérarchie d’héritage, par exemple :
class X { public a: string; constructor(value: string) { this.a = value; }}class Y extends X { public a: string; constructor(value: string) { super(value); this.a = value; }}class Z { public a: string; constructor(value: string) { this.a = value; }}let x: X = new X('x');let y: Y = new Y('y');let z: Z = new Z('z');x === y; // Validx === z; // Valid even if z is from a different inheritance hierarchyLes génériques sont comparés d’après leurs structures, en fonction du type obtenu après application du paramètre générique ; seul le résultat final est comparé comme un type non générique.
interface X<T> { a: T;}let x: X<number> = { a: 1 };let y: X<string> = { a: 'a' };x === y; // Invalid as the type argument is used in the final structureinterface X<T> {}const x: X<number> = 1;const y: X<string> = 'a';x === y; // Valid as the type argument is not used in the final structureLorsque les arguments de type des génériques ne sont pas spécifiés, tous les arguments non spécifiés sont traités comme des types « any » :
type X = <T>(x: T) => T;type Y = <K>(y: K) => K;let x: X = x => x;let y: Y = y => y;x = y; // ValidÀ retenir :
let a: number = 1;let b: number = 2;a = b; // Valid, everything is assignable to itself
let c: any;c = 1; // Valid, all types are assignable to any
let d: unknown;d = 1; // Valid, all types are assignable to unknown
let e: unknown;let e1: unknown = e; // Valid, unknown is only assignable to itself and anylet e2: any = e; // Validlet e3: number = e; // Invalid
let f: never;f = 1; // Invalid, nothing is assignable to never
let g: void;let g1: any;g = 1; // Invalid, void is not assignable to or from anything except anyg = g1; // ValidVeuillez noter que lorsque « strictNullChecks » est activé, « null » et « undefined » sont traités de la même manière que « void » ; dans le cas contraire, ils sont semblables à « never ».
Les types comme ensembles
Dans TypeScript, un type est un ensemble de valeurs possibles. Cet ensemble est également appelé le domaine du type. Chaque valeur d’un type peut être considérée comme un élément d’un ensemble. Un type établit les contraintes que chaque élément de l’ensemble doit respecter pour être considéré comme un membre de cet ensemble. La tâche principale de TypeScript consiste à contrôler et à vérifier si un ensemble est un sous-ensemble d’un autre.
TypeScript prend en charge différents types d’ensembles :
| Terme ensembliste | TypeScript | Remarques |
|---|---|---|
| Ensemble vide | never | « never » ne contient rien en dehors de lui-même |
| Ensemble singleton | undefined / null / literal type | |
| Ensemble fini | boolean / union | |
| Ensemble infini | string / number / object | |
| Ensemble universel | any / unknown | Chaque élément appartient à « any » et chaque ensemble en est un sous-ensemble / « unknown » est l’équivalent de « any » qui préserve la sécurité du typage |
Voici quelques exemples :
| TypeScript | Terme ensembliste | Exemple |
|---|---|---|
| never | ∅ (ensemble vide) | const x: never = ‘x’; // Error: Type ‘string’ is not assignable to type ‘never’ |
| Literal type | Ensemble singleton | type X = ‘X’; |
| type Y = 7; | ||
| Valeur affectable à T | Valeur ∈ T (membre de) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| T1 affectable à T2 | T1 ⊆ T2 (sous-ensemble de) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| const j: XY = ‘J’; // Type ‘“J”’ is not assignable to type ‘XY’. | ||
| T1 extends T2 | T1 ⊆ T2 (sous-ensemble de) | type X = ‘X’ extends string ? true : false; |
| T1 | T2 | T1 ∪ T2 (union) | type XY = ‘X’ | ‘Y’; |
| type JK = 1 | 2; | ||
| T1 & T2 | T1 ∩ T2 (intersection) | type X = { a: string } |
| type Y = { b: string } | ||
| type XY = X & Y | ||
| const x: XY = { a: ‘a’, b: ‘b’ } | ||
| unknown | Ensemble universel | const x: unknown = 1 |
Une union (T1 | T2) crée un ensemble plus large (les deux) :
type X = { a: string;};type Y = { b: string;};type XY = X | Y;const r: XY = { a: 'a', b: 'x' }; // ValidUne intersection (T1 & T2) crée un ensemble plus restreint (uniquement ce qui est partagé) :
type X = { a: string;};type Y = { a: string; b: string;};type XY = X & Y;const r: XY = { a: 'a' }; // Invalidconst j: XY = { a: 'a', b: 'b' }; // ValidDans ce contexte, le mot-clé extends peut être considéré comme signifiant « sous-ensemble de ». Il établit une contrainte pour un type. Lorsque extends est utilisé avec un générique, il limite le paramètre de type générique à un type plus spécifique.
Veuillez noter qu’ici, extends n’a rien à voir avec l’héritage de classes au sens de la POO.
TypeScript utilise des types structurels et ne possède pas de hiérarchie nominale stricte. En réalité, comme dans l’exemple ci-dessous, deux types peuvent se chevaucher sans que l’un soit un sous-type de l’autre, car TypeScript prend en compte la structure, ou forme, des objets.
interface X { a: string;}interface Y extends X { b: string;}interface Z extends Y { c: string;}const z: Z = { a: 'a', b: 'b', c: 'c' };interface X1 { a: string;}interface Y1 { a: string; b: string;}interface Z1 { a: string; b: string; c: string;}const z1: Z1 = { a: 'a', b: 'b', c: 'c' };
const r: Z1 = z; // ValidAttribuer un type : déclarations et assertions de type
Dans TypeScript, un type peut être attribué de différentes manières :
Déclaration de type
Dans l’exemple suivant, nous utilisons x: X (« : Type ») pour déclarer un type pour la variable x.
type X = { a: string;};
// Type declarationconst x: X = { a: 'a',};Si la variable ne respecte pas le format spécifié, TypeScript signale une erreur. Par exemple :
type X = { a: string;};
const x: X = { a: 'a', b: 'b', // Error: Object literal may only specify known properties};Assertion de type
Il est possible d’ajouter une assertion à l’aide du mot-clé as. Cela indique au compilateur que le développeur dispose de plus d’informations sur un type et supprime toutes les erreurs susceptibles de se produire.
Par exemple :
type X = { a: string;};const x = { a: 'a', b: 'b',} as X;Dans l’exemple ci-dessus, l’objet x fait l’objet d’une assertion de type X à l’aide du mot-clé as. Cela indique au compilateur TypeScript que l’objet est conforme au type spécifié, même s’il possède une propriété supplémentaire b absente de la définition du type.
Les assertions de type sont utiles lorsqu’un type plus spécifique doit être indiqué, notamment lors de l’utilisation du DOM. Par exemple :
const myInput = document.getElementById('my_input') as HTMLInputElement;Ici, l’assertion de type as HTMLInputElement sert à indiquer à TypeScript que le résultat de getElementById doit être traité comme un HTMLInputElement. Les assertions de type peuvent également être utilisées pour remapper des clés, comme le montre l’exemple ci-dessous avec les littéraux de gabarit :
type J<Type> = { [Property in keyof Type as `prefix_${string & Property}`]: () => Type[Property];};type X = { a: string; b: number;};type Y = J<X>;Dans cet exemple, le type J<Type> utilise un type mappé avec un littéral de gabarit pour remapper les clés de Type. Il crée de nouvelles propriétés en ajoutant « prefix_ » à chaque clé, et leurs valeurs correspondantes sont des fonctions qui renvoient les valeurs des propriétés d’origine.
Il convient de noter que lors de l’utilisation d’une assertion de type, TypeScript n’effectue pas de vérification des propriétés excédentaires. Il est donc généralement préférable d’utiliser une déclaration de type lorsque la structure de l’objet est connue à l’avance.
Déclarations ambiantes
Les déclarations ambiantes sont des fichiers qui décrivent les types du code JavaScript ; leur nom de fichier respecte le format .d.ts.. Elles sont généralement importées et utilisées pour annoter des bibliothèques JavaScript existantes ou pour ajouter des types aux fichiers JS existants de votre projet.
Les types de nombreuses bibliothèques courantes sont disponibles à l’adresse : https://github.com/DefinitelyTyped/DefinitelyTyped/
et peuvent être installés à l’aide de :
npm install --save-dev @types/library-namePour vos propres déclarations ambiantes, vous pouvez effectuer un import à l’aide de la référence « triple-slash » :
/// <reference path="./library-types.d.ts" />Vous pouvez utiliser des déclarations ambiantes même dans des fichiers JavaScript à l’aide de // @ts-check.
Le mot-clé declare permet de définir des types pour du code JavaScript existant sans l’importer, en servant d’espace réservé aux types provenant d’un autre fichier ou disponibles globalement.
Vérification des propriétés et des propriétés excédentaires
TypeScript repose sur un système de types structurel, mais la vérification des propriétés excédentaires est une fonctionnalité de TypeScript qui lui permet de vérifier si un objet possède exactement les propriétés spécifiées dans le type.
La vérification des propriétés excédentaires est effectuée lors de l’affectation de littéraux d’objet à des variables ou lorsqu’ils sont passés comme arguments à un paramètre de fonction, par exemple.
type X = { a: string;};const y = { a: 'a', b: 'b' };const x: X = y; // Valid because structural typingconst w: X = { a: 'a', b: 'b' }; // Invalid because excess property checkingTypes faibles
Un type est considéré comme faible lorsqu’il ne contient qu’un ensemble de propriétés toutes facultatives :
type X = { a?: string; b?: string;};TypeScript considère comme une erreur l’affectation d’une valeur à un type faible lorsqu’il n’existe aucun chevauchement ; par exemple, le code suivant génère une erreur :
type Options = { a?: string; b?: string;};
const fn = (options: Options) => undefined;
fn({ c: 'c' }); // InvalidBien que cela ne soit pas recommandé, il est possible, si nécessaire, de contourner cette vérification à l’aide d’une assertion de type :
type Options = { a?: string; b?: string;};const fn = (options: Options) => undefined;fn({ c: 'c' } as Options); // ValidOu en ajoutant unknown à la signature d’index du type faible :
type Options = { [prop: string]: unknown; a?: string; b?: string;};
const fn = (options: Options) => undefined;fn({ c: 'c' }); // ValidVérification stricte des littéraux d’objet (fraîcheur)
La vérification stricte des littéraux d’objet, parfois appelée « fraîcheur », est une fonctionnalité de TypeScript qui permet de détecter les propriétés excédentaires ou mal orthographiées qui passeraient autrement inaperçues lors des vérifications structurelles normales des types.
Lors de la création d’un littéral d’objet, le compilateur TypeScript le considère comme « frais ». Si le littéral d’objet est affecté à une variable ou passé en tant que paramètre, TypeScript génère une erreur si le littéral d’objet spécifie des propriétés qui n’existent pas dans le type cible.
Cependant, cette « fraîcheur » disparaît lorsqu’un littéral d’objet est élargi ou qu’une assertion de type est utilisée.
Voici quelques exemples pour illustrer ce comportement :
type X = { a: string };type Y = { a: string; b: string };
let x: X;x = { a: 'a', b: 'b' }; // Freshness check: Invalid assignmentvar y: Y;y = { a: 'a', bx: 'bx' }; // Freshness check: Invalid assignment
const fn = (x: X) => console.log(x.a);
fn(x);fn(y); // Widening: No errors, structurally type compatible
fn({ a: 'a', bx: 'b' }); // Freshness check: Invalid argument
let c: X = { a: 'a' };let d: Y = { a: 'a', b: '' };c = d; // Widening: No Freshness checkInférence de type
TypeScript peut inférer les types lorsqu’aucune annotation n’est fournie lors de :
- L’initialisation d’une variable.
- L’initialisation d’un membre.
- La définition de valeurs par défaut pour les paramètres.
- La détermination du type de retour d’une fonction.
Par exemple :
let x = 'x'; // The type inferred is stringLe compilateur TypeScript analyse la valeur ou l’expression et détermine son type à partir des informations disponibles.
Inférences plus avancées
Lorsque plusieurs expressions interviennent dans l’inférence de type, TypeScript recherche le « meilleur type commun ». Par exemple :
let x = [1, 'x', 1, null]; // The type inferred is: (string | number | null)[]Si le compilateur ne peut pas trouver le meilleur type commun, il renvoie un type union. Par exemple :
let x = [new RegExp('x'), new Date()]; // Type inferred is: (RegExp | Date)[]TypeScript utilise le « typage contextuel » en fonction de l’emplacement de la variable pour inférer les types. Dans l’exemple suivant, le compilateur sait que e est de type MouseEvent grâce au type de l’événement click défini dans le fichier lib.d.ts, qui contient des déclarations ambiantes pour diverses constructions JavaScript courantes et le DOM :
window.addEventListener('click', function (e) {}); // The inferred type of e is MouseEventÉlargissement des types
L’élargissement de type est le processus par lequel TypeScript attribue un type à une variable initialisée sans annotation de type. Il permet de passer de types étroits à des types plus larges, mais pas l’inverse. Dans l’exemple suivant :
let x = 'x'; // TypeScript infers as string, a wide typelet y: 'y' | 'x' = 'y'; // y types is a union of literal typesy = x; // Invalid Type 'string' is not assignable to type '"x" | "y"'.TypeScript attribue string à x à partir de l’unique valeur fournie lors de l’initialisation (x) ; il s’agit d’un exemple d’élargissement.
TypeScript permet de contrôler le processus d’élargissement, par exemple en utilisant « const ».
Const
L’utilisation du mot-clé const lors de la déclaration d’une variable entraîne une inférence de type plus étroite dans TypeScript.
Par exemple :
const x = 'x'; // TypeScript infers the type of x as 'x', a narrower typelet y: 'y' | 'x' = 'y';y = x; // Valid: The type of x is inferred as 'x'En déclarant la variable x avec const, son type est restreint à la valeur littérale spécifique ‘x’. Comme le type de x est plus étroit, il peut être affecté à la variable y sans erreur.
Le type peut être inféré ainsi, car les variables const ne peuvent pas être réaffectées. Leur type peut donc être restreint à un type littéral spécifique, ici le type littéral ‘x’.
Modificateur Const sur les paramètres de type
Depuis la version 5.0 de TypeScript, il est possible de spécifier l’attribut const sur un paramètre de type générique. Cela permet d’inférer le type le plus précis possible. Voyons un exemple sans utiliser const :
function identity<T>(value: T) { // No const here return value;}const values = identity({ a: 'a', b: 'b' }); // Type inferred is: { a: string; b: string; }Comme vous pouvez le constater, les propriétés a et b sont inférées avec un type string.
Voyons maintenant la différence avec la version utilisant const :
function identity<const T>(value: T) { // Using const modifier on type parameters return value;}const values = identity({ a: 'a', b: 'b' }); // Type inferred is: { a: "a"; b: "b"; }Nous pouvons maintenant constater que les propriétés a et b sont inférées comme des littéraux de chaîne plutôt que comme de simples types string.
Assertion Const
Cette fonctionnalité permet de déclarer une variable avec un type littéral plus précis à partir de sa valeur d’initialisation, ce qui indique au compilateur que la valeur doit être traitée comme un littéral immuable. Voici quelques exemples :
Sur une seule propriété :
const v = { x: 3 as const,};v.x = 3;Sur un objet entier :
const v = { x: 1, y: 2,} as const;Cela peut être particulièrement utile lors de la définition du type d’un tuple :
const x = [1, 2, 3]; // number[]const y = [1, 2, 3] as const; // Tuple of readonly [1, 2, 3]Annotation de type explicite
Nous pouvons être explicites et fournir un type. Dans l’exemple suivant, la propriété x est de type number :
const v = { x: 1, // Inferred type: number (widening)};v.x = 3; // ValidNous pouvons rendre l’annotation de type plus précise en utilisant une union de types littéraux :
const v: { x: 1 | 2 | 3 } = { x: 1, // x is now a union of literal types: 1 | 2 | 3};v.x = 3; // Validv.x = 100; // InvalidRéduction
La réduction de type est le processus par lequel TypeScript réduit un type général à un type plus spécifique. Cela se produit lorsque TypeScript analyse le code et détermine que certaines conditions ou opérations permettent d’affiner les informations de type.
La réduction des types peut se produire de différentes manières, notamment avec :
Conditions
En utilisant des instructions conditionnelles, telles que if ou switch, TypeScript peut réduire le type en fonction du résultat de la condition. Par exemple :
let x: number | undefined = 10;
if (x !== undefined) { x += 100; // The type is number, which had been narrowed by the condition}Lever une exception ou retourner une valeur
Lever une exception ou effectuer un retour anticipé depuis une branche peut aider TypeScript à réduire un type. Par exemple :
let x: number | undefined = 10;
if (x === undefined) { throw 'error';}x += 100;Parmi les autres façons de réduire les types dans TypeScript figurent :
- L’opérateur
instanceof: utilisé pour vérifier si un objet est une instance d’une classe spécifique. - L’opérateur
in: utilisé pour vérifier si une propriété existe dans un objet. - L’opérateur
typeof: utilisé pour vérifier le type d’une valeur lors de l’exécution. - Les fonctions intégrées telles que
Array.isArray(): utilisées pour vérifier si une valeur est un tableau.
Union discriminée
L’utilisation d’une « union discriminée » est un modèle dans TypeScript où une « étiquette » explicite est ajoutée aux objets afin de distinguer les différents types d’une union. Ce modèle est également appelé « union étiquetée ». Dans l’exemple suivant, l’« étiquette » est représentée par la propriété « type » :
type A = { type: 'type_a'; value: number };type B = { type: 'type_b'; value: string };
const x = (input: A | B): string | number => { switch (input.type) { case 'type_a': return input.value + 100; // type is A case 'type_b': return input.value + 'extra'; // type is B }};Gardes de type définies par l’utilisateur
Lorsque TypeScript ne parvient pas à déterminer un type, il est possible d’écrire une fonction auxiliaire appelée « garde de type définie par l’utilisateur ». Dans l’exemple suivant, nous utiliserons un prédicat de type pour réduire le type après l’application d’un filtrage :
const data = ['a', null, 'c', 'd', null, 'f'];
const r1 = data.filter(x => x != null); // The type is (string | null)[], TypeScript was not able to infer the type properly
const isValid = (item: string | null): item is string => item !== null; // Custom type guard
const r2 = data.filter(isValid); // The type is fine now string[], by using the predicate type guard we were able to narrow the typeRéduction avec switch-true
TypeScript 5.3 ajoute la réduction avec switch-true, qui permet de remplacer des chaînes if/else peu lisibles par switch (true) utilisant des conditions booléennes. Elle améliore la lisibilité tout en continuant à réduire les types. Elle s’apparente à la correspondance de motifs, mais en plus simple.
function classify(x: unknown) { switch (true) { case typeof x === 'string': return `"${x.toUpperCase()}"`; case typeof x === 'number': return x > 0 ? 'positive' : 'negative'; case Array.isArray(x): return `[${x.length} items]`; default: return 'something else'; }}