Contenu principal

Maßtriser les types avancés en TypeScript

lundi 28 février 2024

La semaine derniùre, je vous parlais des 3 rùgles d’or en TypeScript.

La troisiÚme est certainement la plus difficile à mettre en place : préférer la dérivation de types.

Ce que j’entends par lĂ , c’est que le systĂšme de typage va ĂȘtre vraiment efficace Ă  partir du moment oĂč vous ĂȘtes prĂ©cis dans sa dĂ©finition. Mais plus vous ĂȘtes prĂ©cis, plus vous risquez de dupliquer les types et rendre le code inmaintenable. La solution est alors de recourir Ă  la dĂ©rivation (= dĂ©finir un type Ă  partir d’un autre).

Mais ce n’est pas facile. Alors aujourd’hui, je vais vous prĂ©senter pas Ă  pas quelques notions clĂ©s qui vous permettront de configurer vos types au mieux. En assimilant au fur et Ă  mesure ces concepts, vous devriez pouvoir devenir autonome pour typer n’importe quelle partie de votre code đŸ’Ș

Est-ce que cet article est fait pour vous ? Il ne s’agit pas d’un article d’introduction, il vous faut donc avoir dĂ©jĂ  utilisĂ© TypeScript et avoir croisĂ© la notion de gĂ©nĂ©ricitĂ©. Mon article de la semaine derniĂšre peut ĂȘtre une bonne premiĂšre lecture pour introduire ce qu’est la gĂ©nĂ©ricitĂ©. L’objectif n’est pas forcĂ©ment de tout assimiler d’un coup – certains aspects mettent du temps Ă  rentrer – mais de mettre en place les briques qui consolideront votre comprĂ©hension de TypeScript. Voyez cela un peu comme un catalogue de techniques qui pourront vous ĂȘtre utiles au quotidien.


Notions abordées :

RĂ©cupĂ©ration des clĂ©s d’un objet en TypeScript

Objectif: Restreindre le type d’une variable pour pouvoir l’utiliser directement en tant que clĂ© d’un objet.

Prenons l’exemple d’un objet constituĂ© de plusieurs clĂ©s :

type Config = {
	sku: string;
	name: string;
	quantity: number;
};

A partir de cette config, je veux pouvoir Ă©crire la fonction getConfig, qui prend en deuxiĂšme paramĂštre une des clĂ©s de l’objet :

function getConfig(config, key) {
	return config[key];
}

Sans TypeScript, key pourrait ĂȘtre n’importe quoi, y compris "toto" qui ne fait pas partie des config disponibles.

On va donc chercher à la contraindre pour que ce ne soit pas juste une string mais plutÎt une union des différentes possibilités.

// ❌ on Ă©vite de l'Ă©crire manuellement
type ConfigKey = 'sku' | 'name' | 'quantity';

Pour récupérer automatiquement cette union, on utiliser le mot clé keyof:

// ✅ on lui prĂ©fĂšre keyof
type ConfigKey = keyof Config;

Maintenant que l’on sait rĂ©cupĂ©rer le type de la clĂ©, revenons Ă  notre fonction : comment faire pour correctement typer la clĂ© et la valeur de retour ?

Le type de la clĂ©, c’est keyof Config comme on vient de le voir. Mais le type de retour ? Le premier rĂ©flexe, serait d’utiliser Config[keyof Config].

// ❌ ne fonctionne pas comme attendu
function getConfig(
	config: Config,
	key: keyof Config
): Config[keyof Config] {
	return config[key];
}

Mais ça ne va pas fonctionner parce que si on déconstruit Config[keyof Config], on aurait :

Config[keyof Config]
<=> Config['sku' | 'name' | 'quantity']
<=> Config['sku'] | Config['name'] | Config['quantity']
<=> string | string | number
<=> string | number

Or, si je fais getConfig(config, 'quantity'), je m’attends à ce que la valeur de retour soit un number uniquement, pas une string.

Donc on va devoir passer par de la gĂ©nĂ©ricitĂ©. Pour revoir l’explication de la syntaxe, n’hĂ©sitez pas Ă  revenir Ă  l’article de la semaine derniĂšre.

function getConfig<
  Key extends keyof Config,
>(config: Config, key: Key): Config[Key] {
  return config[key];
}

Ainsi, le type de Key est une des valeurs disponibles dans l’union, et pas l’union entiùre.

GrĂące Ă  cette mĂ©thode, on a vraiment le paramĂštre key qui ne peut ĂȘtre qu’une clĂ© de l’objet config et donc la valeur de retour sera le type de cette clĂ© uniquement. Ca nous donne un filet de sĂ©curitĂ©, et ça nous permet d’avoir une autocomplĂ©tion aux petits oignons :

Lorsqu'on écrit le code getConfig({ sku: "123", name: "Product" }, '') dans l'IDE, l'autocomplétion du deuxiÚme paramÚtre est name et sku.

Exercice

Dans cet exemple on a utilisĂ© le type Config dĂ©fini en amont. Comment feriez-vous pour que ça fonctionne pour n’importe quel objet ?

Solution :

La solution est d’ajouter un nouveau paramùtre de type. C’est ce qui permettra d’adapter la config en fonction de l’objet que vous passez à la fonction.

 function getConfig<
+  Config extends Record<string, unknown>,
   Key extends keyof Config,
 >(config: Config, key: Key): Config[Key] {
   return config[key];
 }

⚠ Attention cependant, ça n’a du sens que si vous voulez faire une fonction qui marche avec n’importe quel type de config. S’il n’y a qu’une seule forme de config Ă  travers toute l’application, ça n’a sĂ»rement pas d’intĂ©rĂȘt de la rendre gĂ©nĂ©rique.

Filtrer les clĂ©s d’un objet en TypeScript

Objectif : Apprendre à transformer le type d’un objet

La semaine derniÚre, je vous parlais du type utilitaire Omit<T, key>. Celui-ci permet de retirer une ou plusieurs clés de votre objet.

type Config = {
	sku: string;
	name: string;
	quantity: number;
};

type LimitedConfig = Omit<Config, 'quantity'>;
// { sku: string, name: string }

Pour lui retirer plusieurs clés :

type LimitedConfig = Omit<Config, 'quantity' | 'name'>;
// { sku: string }

Mais comment feriez vous si vous vouliez plutĂŽt filtrer les clĂ©s en fonction de leur valeur ? Par exemple, je ne veux rĂ©cupĂ©rer que les clĂ©s dont la valeur associĂ©e est une string ?

Commençons par dupliquer le type en disant : je crĂ©e un nouveau type StringConfig qui a exactement les mĂȘmes clĂ©s que Config (Key in keyof Config), et pour chaque clĂ© je lui associe la mĂȘme type de valeur que Config (Config[Key]).

// ❌ pas encore complet
type StringConfig = {
	[Key in keyof Config]: Config[Key];
};

Le problĂšme c’est que pour l’instant je n’ai pas filtrĂ© mes valeurs. Pour ça, l’astuce est de dire Ă  TypeScript : si Config[Key] est de type string, alors je l’utilise, sinon je lui associe le type never pour dire Ă  TypeScript qu’on n’a pas le droit d’utiliser cette clĂ©.

// ❌ pas encore complet
type StringConfig = {
	[Key in keyof Config]:
		Config[Key] extends string
			? Config[Key]
			: never;
};
L'autocompletion de stringConfig.quantity montre un type never

On a bien un type never sur la quantity plutĂŽt qu’un number. Mais ce n’est pas totalement satisfaisant parce que la clĂ© quantity existe toujours. C’est un peu trompeur pour la personne qui utilisera le type plus tard.

On va donc retirer toutes les clĂ©s qui retournent un type never. Pour ça j’ai besoin de 2 Ă©tapes :

  1. Je rĂ©cupĂšre toutes les clĂ©s dont la valeur n’est pas never. Pour cela, je reprends le mĂȘme code que plus haut Ă  2 diffĂ©rences prĂšs :

    type StringConfigKeys = {
    	[Key in keyof Config]:
    		Config[Key] extends string
    			? Key
    //			  ^^^ je retourne la clé plutÎt que sa valeur
    			: never;
    }[keyof Config]
    //^^^^^^^^^^^^^ puis je récupÚre toutes les clés
    //              plutĂŽt que l'objet { [key]: key }
    

    Ainsi, TypeScript va comprendre que StringConfigKeys = 'sku' | 'name'. L’intĂ©rĂȘt Ă©tant que la clĂ© quantity a complĂštement disparu.

  2. Je peux ensuite reconstruire l’objet entier en ne rĂ©cupĂ©rant que les clĂ©s dĂ©finies Ă  l’étape 1, grĂące au type utilitaire Pick

    type StringConfig = Pick<Config, StringConfigKeys>;
    

Et voilĂ , vous avez votre nouveau type qui a Ă©tĂ© convenablement filtrĂ© 🎉

Exercice

Potentiellement devoir refaire cette gymnastique à chaque fois, c’est pas trùs pratique. Alors à la place, comment est-ce que vous coderiez les types :

  • RemoveNeverValues<O> qui prend un objet O, et renvoie un nouveau type oĂč toutes les clĂ©s dont la valeur est never ont disparu
  • FilterByValue<O, V> qui prend un objet O, et qui ne conserve que les clĂ©s dont la valeur est du mĂȘme type que V

Ainsi, vous pourriez écrire:

type StringConfig = FilterByValue<Config, string>;

Solution :

// Retire toutes les clés dont la valeur est `never`
type RemoveNeverValues<T> = Pick<
	T,
	{
		[K in keyof T]: T[K] extends never ? never : K;
	}[keyof T]
>;

// Retire du type T toutes les clés qui ont pour valeur V
type FilterByValue<T, V> = RemoveNeverValues<{
	[Key in keyof T]: T[Key] extends V ? T[Key] : never;
}>;

// Ce qui nous permet d'avoir enfin
// ce type filtrĂ© fonctionnel ✅
type StringConfig = FilterByValue<Config, string>;
// { sku: string, name: string }

Manipuler des class en TypeScript

Objectif : Extraire les mĂ©thodes disponibles d’une class

En programmation orientĂ©e objet, il est frĂ©quent de passer par l’utilisation d’interfaces pour dĂ©crire les mĂ©thodes attendues dans une class. Cela pourrait ressembler Ă  ceci :

type ClockInterface = {
	getCurrentTime(): Date;
};

class Clock implements ClockInterface {
	currentTime: Date = new Date();

	constructor(h: number, m: number) {
		// Votre implémentation
	}

	getCurrentTime() {
		return this.currentTime;
	}
}
Si vous vous interrogez sur l'utilisation de interface vs type. N'hésitez pas à déplier cette explication pour en savoir plus.

Il existe 2 différences entre type et interface :

  1. Le principal bĂ©nĂ©fice de interface est qu’il est possible de faire de l’hĂ©ritage via le mot clĂ© extends. Cela mimique donc trĂšs bien un hĂ©ritage classique en POO.

    interface AnimalInterface {
    	eat(): void;
    }
    
    interface DogInterface extends AnimalInterface {
    	bark(): string;
    }
    
  2. L’autre diffĂ©rence est que si vous dĂ©clarez la mĂȘme interface plusieurs fois, alors elles vont se fusionner entre elles.

    interface Window {
    	addEventListener: EventListener;
    }
    
    interface Window {
    	body: HTMLBodyElement;
    }
    
    // alors tout objet typé avec Window
    // devra avoir à la fois la méthode
    // addEventListener et un body
    

    A l’inverse, quand on dĂ©clare deux types avec le mĂȘme nom, cela dĂ©clenchera une erreur (Error: Duplicate identifier 'Window'.).

    Ainsi, pour Ă©viter de trop Ă©parpiller ma dĂ©finition de type, je prĂ©fĂšre utiliser type. Ca ne m’empĂȘche pas d’utiliser de temps en temps interface, mais uniquement quand je ne peux pas faire autrement (systĂšme de plugins oĂč quelqu’un peut Ă©tendre une API existante).

❓ Si on regarde ClockInterface, on constate qu’on n’a pas typĂ© le constructor ? Comment faire pour aussi forcer le type du constructeur ?

En fait, lorsqu’on code une class, on crĂ©e en rĂ©alitĂ© deux choses distinctes :

  1. le constructeur : qui est là pour construire un objet (= la fonction qui est appelée quand on fait new)
  2. l’instance : qui est l’objet qu’on manipule aprùs avoir fait un instance = new Class()

Donc si vous voulez représenter le type du constructeur, il faut plutÎt écrire :

type ClockConstructor = new (hour: number, minute: number): Clock;

💡 Plus gĂ©nĂ©ralement, vous pouvez utiliser ce type gĂ©nĂ©rique lors que vous introduisez des variables de types :

export type Constructor<T> = new (...args: unknown[]) => T;

// Ou bien celui-ci pour ajouter des propriétés statiques
export type ConstructorWithStatics<T, S extends Record<string, unknown>> = Constructor<T> & S;

A l’inverse, si Ă  partir d’un constructeur vous voulez pouvoir rĂ©cupĂ©rer l’instance, vous pouvez utiliser le type natif de TypeScript:

export type Clock = InstanceType<ClockConstructor>;

❓ Quand est-ce que ça peut nous ĂȘtre utile ?

Un exemple que vous pouvez trouver dans la nature, c’est pour des systùmes de routing. Prenons celui d’AdonisJS :

router.get('users', [UsersController, 'all']);

Pour déclarer une URL, vous pouvez associer une URL (ici /users) à un handler qui appartient à un Controller (ici UsersController::all). Et ce Controller ressemblera à quelque chose de ce style :

class UsersController {
	async all(ctx: HttpContext) {
		// Ca fait d'autres trucs
	}

	async store(ctx: HttpContext) {
		// Ca fait des trucs
	}
}

Notre but, c’est de bien typer router.get pour dans le deuxiùme paramùtre on ne puisse que utiliser store ou all. Il faut que ce soit impossible d’utiliser une autre chaüne de caractùres. On va donc essayer de coder le type Handler ci-dessous.

function post(path: string, handler: Handler): void;

PremiÚrement, un type Handler est un tableau qui contient deux valeurs :

  1. Une class qui représente le controller
  2. Une string qui spécifie quelle méthode du controller on doit appeler
// ❌ pas encore complet
type Handler = [Constructor<unknown>, string];

Sauf qu’on a dit qu’on ne voulait pas juste une string, mais uniquement les mĂ©thodes qui appartiennent au controller. De fait, on va devoir utiliser une dĂ©rivation de type et donc utiliser de la gĂ©nĂ©ricité :

// ❌ pas encore complet
type Handler<
    C extends Constructor<unknown>
> = [C, keyof InstanceType<C>];

Décortiquons ce keyof InstanceType<C> :

  1. On sait que c’est quelque chose qu’on va devoir dĂ©river du Constructor passĂ© en entrĂ©e, donc c’est pour cette raison qu’on crĂ©e cette variable de type C
  2. Cependant, je vous avais dit que le Constructor ce n’était qu’un new, il ne contient pas rĂ©ellement les mĂ©thodes associĂ©es Ă  la classe. C’est pour cette raison qu’on utiliser InstanceType<C>.
  3. Mais InstanceType<C> retourne en fait une sorte d’objet qui a pour clĂ©s le nom des mĂ©thodes, et en valeur le type de la fonction associĂ©e. Nous, on ne veut que les clĂ©s, donc on ajoute keyof

💡 En rĂ©alitĂ© InstanceType<C> retournerait aussi les propriĂ©tĂ©s publiques de l’objet. Il ne contient pas que les fonctions. Mais dans notre cas, notre Controller n’a que des fonctions pour l’instant.

Ainsi, j’ai bien :

type ControllerMethodKeys = Handler<typeof UsersController>;
// [UsersController, 'store' | 'all']

Cela dit, ce n’est pas encore parfait. Admettons que dans mon UsersController j’ai une autre mĂ©thode, mais que sa signature n’est pas compatible parce qu’elle ne commence pas par ctx: HttpContext :

class UsersController {
	async store(ctx: HttpContext) {}
	async all(ctx: HttpContext) {}

	async renderUser(user: User) {
		// Ca fait d'autres trucs
	}
}

đŸ€« Il faudrait sĂ»rement que cette mĂ©thode soit privĂ©e dans cet exemple, mais faisons comme si ce n’était pas le cas.

Il ne faudrait pas que je puisse écrire [UsersController, 'renderUser'].

Pour y arriver, plutĂŽt que de retourner les clĂ©s de InstanceType<C>, je ne retourne que les clĂ©s dont la valeur associĂ©e respecte le type (ctx: HttpContext) => void | Promise<void>. Je peux donc rĂ©utiliser le helper que je vous avais prĂ©sentĂ© Ă  la fin de la section Filtrer les clĂ©s d’un objet : FilterByValue.

// ✅ version finale
type Handler<C extends Constructor<unknown>> = [
	C,
	keyof FilterByValue<
        InstanceType<C>,
        (ctx: HttpContext) => void | Promise<void>
    >
];

function post<C extends Constructor<unknown>>(path: string, handler: Handler<C>): void;

Ce qui me permet donc d’avoir Ă  nouveau une autocomplĂ©tion aux petits oignons 🧅

Lorsqu'on déclenche l'autocomplétion sur la deuxiÚme partie du handler [UsersController, ''], on voit apparaßtre uniquement les options "all" et "store"

Manipuler des strings en TypeScript

Objectif : Créer des clés dynamiques dans un objet

On a pu voir prĂ©cĂ©demment que le type string n’est pas identique au type 'sku' | 'name' | 'quantity'.

Il est donc possible d’apporter beaucoup de sĂ»retĂ© Ă  la gestion des strings. Et notamment, ce qui va permettre de faire ça, c’est la gestion des Template Literals en TypeScript.

En JavaScript, un template literal, c’est la syntaxe suivante :

const hello = `Hello ${name}`;

Et bien, en TypeScript, on va pouvoir faire la mĂȘme chose aux niveaux des types :

type WithId<S extends string> = `${S}Id`;

type UserId = WithId<'user'>;
// type: "userId"

❓ Comment faire alors pour ajouter des clĂ©s dynamiquement Ă  un objet ?

Je veux passer de cet objet :

type Values = {
	name?: string;
	quantity?: number;
};

A celui-ci :

type HasValues = {
	hasName: boolean;
	hasQuantity: boolean;
};

đŸ€« Je vous dĂ©conseille de rĂ©flĂ©chir votre code de cette façon quand vous faites du TypeScript, le premier type est suffisant. Mais j’ai eu besoin de faire ce genre de code pour Stimulus, une librairie qui Ă©tait difficilement compatible avec TypeScript. Et c’est un bon cas pratique pour manipuler des Template Literals.

Si vous ĂȘtes curieux de cette histoire de Stimulus, sachez que je sors un article Ă  ce sujet semaine prochaine, alors n’hĂ©sitez pas Ă  aller me suivre sur les rĂ©seaux sociaux \o/ (Mastodon, LinkedIn, Twitter ou flux RSS)

Commençons par transformer une string XXX en hasXXX :

  • on peut utiliser un Template Literal pour prĂ©fixer par has
  • par contre, on veut que ce soit hasName et non hasname, donc on doit utiliser Capitalize pour rajouter la majuscule dont on a besoin
type HasNameKey = `has${Capitalize<'name'>}`;
// type 'hasName'

Maintenant, on doit faire ça pour chacune des clés de mon objet. On va donc se retrouver avec le code suivant :

type HasValues = {
	[Key in `has${Capitalize<keyof Values>}`]: boolean;
};

💡 Ce qui peut vous paraĂźtre Ă©trange, c’est le positionnement du mot clĂ© keyof. Quand on fait du code classique, on a tendance Ă  Ă©cire for (let key of values) et Ă  l’intĂ©rieur seulement, on fait les transformations. Ici, ça a l’air d’ĂȘtre inversé : le keyof est Ă  l’intĂ©rieur de toutes les transformations.

Ce qui m’a donnĂ© le dĂ©clic c’est quand j’ai compris que ces deux bouts de code sont Ă©quivalents :

type Key = Capitalize<'name' | 'quantity'>;

type Key = Capitalize<'name'> | Capitalize<'quantity'>;

Donc plutĂŽt que de chercher Ă  construire soi mĂȘme chaque cas et de mettre des | au milieu, on va essayer de mettre les | le plus Ă  l’intĂ©rieur possible pour Ă©viter d’avoir Ă  tout réécrire.

Et donc, comment rĂ©cupĂ©rer l’union 'name' | 'quantity' ? En mettant keyof :

Capitalize<keyof Values>;

Extraire un type d’un type existant grñce à infer en TypeScript

Objectif : rĂ©cupĂ©rer le type interne d’un type existant

Enfin, la derniĂšre brique vraiment importante pour bien comprendre les systĂšmes de types, c’est comment extraire un type d’un autre. C’est ce qui vous permettra de bien manipuler vos types et limitera le nombre de variables de types que vous passerez Ă  vos gĂ©nĂ©riques.

Pour apprendre à faire ça, commençons par observer comment sont typées les fonctions :

function sum(a: number, b: number): number;

En réalité, si on revient uniquement à la définition de son type, ça nous donnerait quelque chose de ce style :

type Sum = (a: number, b: number) => number;

❓ Comment faire pour rĂ©cupĂ©rer le type de retour uniquement Ă  partir de Sum ?

Si jamais vous avez dĂ©jĂ  parcouru la doc de TypeScript, vous aurez peut-ĂȘtre remarquĂ© qu’il existe le type utilitaire ReturnType. Mais admettons que nous devions la recoder cette classe utilitaire. Comment ça marcherait ?

type ReturnType<F extends Function> = ???;

Une premiÚre façon serait de faire comme précédemment : on décrit un peu mieux la contrainte pour pouvoir utiliser un bout de celle-ci. Je remplace donc Function par :

// ❌ ne fonctionne pas
type ReturnType<F extends (...args: unknown[]): Result> = Result;

Le problùme c’est que la variable de type Result n’existe pas pour l’instant. Si on devait la rajouter, il faudrait la mettre avant F. Sauf que nous, on a que Sum de disponible.

La solution rĂ©side dans le mot clĂ© infer. Celui-ci indique Ă  TypeScript d’essayer de deviner le type, et de l’utiliser selon s’il a rĂ©ussi ou non. Plus concrĂštement, ça veut dire qu’on va pouvoir faire une condition (vu qu’il faut gĂ©rer le cas oĂč il n’a pas rĂ©ussi) qui ressemble Ă  ceci :

// ✅ ça marche
type ReturnType<
    /* 1 */
    F extends (...args: never[]) => unknown
> =
    /* 2 */
	F extends (...args: never[]) => infer Result
		? Result /* 3 */
		: never /* 4 */;
  1. On limite quand mĂȘme l’utilisation de ReturnType<F> aux fonctions qu’on peut infĂ©rer. Sinon, je peux lui passer un objet, et il me retournera bĂȘtement never, au lieu de me prĂ©venir que le type est incompatible.
  2. On utilise ensuite le mot clĂ© infer pour demander Ă  TypeScript s’il arrive Ă  comprendre le type Ă  l’endroit qui nous intĂ©resse (ici en retour de fonction).
  3. Si oui, alors TypeScript nous met ce type dans la variable de type Result qu’on a dĂ©fini avec le mot clĂ© infer : on l’utilise donc directement pour indiquer que c’est notre type final.
  4. Si non, on retourne un autre type : trĂšs souvent never pour dire que ce n’est pas possible. Dans ce cas prĂ©cis, on ne tombera jamais de ce cĂŽtĂ© de la condition parce qu’en /* 1 */ on a bien ciblĂ© le type qu’on pouvait recevoir en entrĂ©e. Mais on est quand mĂȘme obligĂ© de le renseigner.

💡 Vous aurez peut ĂȘtre constatĂ© que j’ai Ă©crit ...args: never[] et non unknown[]. L’explication se trouve dans une histoire de variance. La diffĂ©rence est importante, mais n’est pas vraiment important Ă  comprendre : utilisez celui qui fonctionne 😁

C’est bien beau, mais on a juste refait du code qui existait dĂ©jĂ . Voyons comment on pourrait l’appliquer dans un contexte un peu diffĂ©rent.

Pour cela je vais reprendre l’exemple prĂ©cĂ©dent :

type Values = {
	name?: string;
	quantity?: number;
};

type HasValues = {
	[Key in `has${Capitalize<keyof Values>}`]: boolean;
};

J’aimerais, Ă  partir d’une clĂ© de HasValues pouvoir revenir Ă  ma clĂ© initiale.

BaseValueKey<'hasName'>; // name
BaseValueKey<'hasQuantity'>; // quantity

Comment est-ce qu’on pourrait typer BaseValueKey ?

En utilisant les mĂȘmes 4 Ă©tapes :

type BaseValueKey<
	/* 1 */
	HasKey extends keyof HasValues
> =
	/* 2 */
	HasKey extends `has${infer Key}`
		? Uncapitalize<Key> /* 3 */
		: never /* 4 */;
  1. On pense toujours Ă  limiter au maximum les paramĂštres qu’on peut passer en entrĂ©e (ici uniquement les clĂ©s de HasValues)
  2. On utilise le mot clĂ© infer lĂ  oĂč rĂ©side la valeur qui nous intĂ©resse.
  3. Si TypeScript a rĂ©ussi Ă  comprendre le type, on le retourne. Mais ici on a la petite subtilitĂ© qu’on l’avait mis en majuscule. Alors il faut inverser l’inverser en utilisant Uncapitalize<T>
  4. S’il n’a pas compris, on retourne le type par dĂ©faut – la plupart du temps never

Le mot clĂ© infer peut s’utiliser dans pas mal de situations. On vient de voir notamment les arguments/retours de fonction, dans les template literals. Mais c’est aussi le cas pour les types gĂ©nĂ©riques.

Exercice

Comment feriez-vous pour typer ElementType<T> qui extraire le type des Ă©lĂ©ments d’un tableau ?

Solution :

type ElementType<A extends Array<unknown>> =
	A extends Array<infer Item>
		? Item
		: never;

Conclusion

Toutes ces techniques, mises bout Ă  bout, vous permettront de vraiment configurer en dĂ©tail votre systĂšme de typage. Gardez en tĂȘte toutefois que le but est d’arriver Ă  une base de code qui est plus facile Ă  gĂ©rer :

  • si finalement, chaque ligne de code devient plus compliquĂ©e, et ça ne vous apporte rien de plus que vos tests automatisĂ©s, privilĂ©giez une solution plus simple
  • si au contraire, ça vous permet de bĂ©nĂ©ficier d’une meilleure sĂ©curitĂ© et que vous ĂȘtes sur un bout de code qui est utilisĂ© partout dans votre code base, alors ça vaut le coup d’investir un peu de temps maintenant pour vous faciliter la vie plus tard.

Je continuerai d’agrĂ©menter le contenu de cet article dans le temps avec d’autres cas qui peuvent ĂȘtre utiles. Si vous en voyez d’autres, n’hĂ©sitez donc pas Ă  me contacter.

Si vous ĂȘtes en manque d’inspiration, pas de panique : la semaine prochaine, je publierai un cas concret de comment ajouter des types sur du code JS qui n’a pas rĂ©ellement Ă©tĂ© pensĂ© pour. Notamment avec le cas d’usage de Stimulus, un framework front particuliĂšrement utilisĂ© dans la communautĂ© Ruby on Rails et Symfony (UX). N’hĂ©sitez donc pas Ă  me suivre (Mastodon, LinkedIn, Twitter ou flux RSS) pour ne pas rater ça 😘

En attendant, voici quelques ressources qui peuvent vous ĂȘtre utiles pour approfondir tout ça :


Si vous voulez suivre mes publications, il paraĂźt que j'ai un feed RSS, Mastodon et un Twitter.

Si vous pensez à d'autres méthodes que vous voudriez que je mette en place (pigeon voyageur, avion en papier, etc.), n'hésitez pas à me les proposer :)