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
- Filtrer les clĂ©s dâun objet
- Manipuler des
class
- Manipuler des strings
- Extraire un type dâun type existant grĂące Ă
infer
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 :

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

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 :
-
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. -
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 objetO
, et renvoie un nouveau type oĂč toutes les clĂ©s dont la valeur estnever
ont disparuFilterByValue<O, V>
qui prend un objetO
, et qui ne conserve que les clĂ©s dont la valeur est du mĂȘme type queV
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
 :
-
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; }
-
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 tempsinterface
, 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 :
- le constructeur : qui est là pour construire un objet (= la fonction qui est appelée quand on fait
new
) - 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 :
- Une class qui représente le controller
- 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>
 :
- 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
- 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 utiliserInstanceType<C>
. - 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 ajoutekeyof
đĄ 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"](/images/posts/typescript/handler.png)
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 nonhasname
, donc on doit utiliserCapitalize
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 */;
- 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ĂȘtementnever
, au lieu de me prévenir que le type est incompatible. - 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). - 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. - 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 nonunknown[]
. 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 */;
- On pense toujours Ă limiter au maximum les paramĂštres quâon peut passer en entrĂ©e (ici uniquement les clĂ©s de
HasValues
) - On utilise le mot clé
infer
lĂ oĂč rĂ©side la valeur qui nous intĂ©resse. - 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>
- 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 :
- Total TypeScript (EN)Â : les articles et les tips de Matt Pocock
- types-challenges, un repo avec plein dâexercices pour sâentraĂźner Ă la manipulation de TypeScript
- TypeType : un preprocesseur pour TypeScript (je nâai pas encore eu le temps de creuser, mais dans certains cas vraiment compliquĂ©s, ça peut permettre de simplifier la maintenance)
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 :)