> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Typescript Toolbox: Interpolation typée
- URL: https://www.sfeir.dev/front/typescript-toolbox-interpolation-typee/
- Published: 2023-07-05T09:56:09.000Z
- Updated: 2023-11-10T16:40:57.000Z
- Description: Dans cette série d'articles, nous aborderons des techniques pour tirer parti de Typescript et de l'outillage disponible dans nos IDE. Dans ce premier épisode, nous abordons un problème d'interpolation de chaînes de caractères.
- Author: Julien Tanguy
- Tags: Front, Back

Dans cette série d’articles, nous aborderons ensemble quelques patterns et techniques pour nous permettre de tirer pleinement parti de Typescript et de l’outillage disponible dans nos IDE. Dans ce premier épisode, nous aborderons un problème d’interpolation de chaînes de caractères. À partir d’un ensemble de messages avec des trous, comment peut-on dériver un objet avec les infos à remplir à partir de l’objet de paramètres qui contient différents messages à trous ?

## Implémentation initiale

Tout au long de cet article, nous allons nous baser sur un objet `DESCRIPTIONS` , qui contient en clé le code du message, et en valeur son contenu. Nous avons également une fonction `fillDescription` qui prend en paramètre le code du message et un objet de paramètres, et remplace dans le message les variables marquées entre accolades.

```typescript
const DESCRIPTIONS: Record<string, string> = {
    HELLO: "Bienvenue {nom}",
    UPGRADE_SUBSCRIPTION: "Demande de changement d'abonnement {abonnementSource} vers {abonnementCible} à partir du {date}",
    CANCEL_SUBSCRIPTION: "Demande d'arrêt de l'abonnement {abonnement} à partir du {date}",
    SHARE_SOCIAL: "Partager l'article {titre} sur {reseauSocial}",
};

/**
 * Remplissage de contenu d'un message
 * @param code le code du message à remplir
 * @param params les différents paramètres du message
 * @returns un message complet
 */
export function fillDescription(
    code: string,
    params: Record<string, string>
) {
    // interpolate params
    const message = Object.entries<string>(params).reduce<string>(
        (msg, [key, value]) => msg.replace(`{${key}}`, value),
        DESCRIPTIONS[code]
    );
    return message;
}
```

Implémentation initiale. [Lien vers le **playground* typescript](https://www.typescriptlang.org/play?ts=5.1.3&ref=sfeir.dev#code/MYewdgzgLgBAIgUQMoGEBKBJACgFQweQDkkAuGNAU1ACcATAHmmoEswBzAGhidbYD4YAXhgBvAFAxJMABIIAMnPxkARACFmFMADdNAVwqiwIALYBfZRwlSAqlgDiaAIKIA+kmurUmXAUIq4FMYAhmC0BmEwwAAWIWyBmrC0AORBAEbgYPFgsCJpGVlQSCC61MAUpjA61BCieWCZxgkozKkANuUwAAcwAA5B1FDM1DC0uqK0QVDlFlaSKI6EKPJuHl7YeET+gSERyf3UAFeJBq0p6fUFtecNCRXdfQNDI2MiE1PmllIwSNKOaAhufAoDCOOQqLD9KBBOLDU6Q5jAdqiQZQagdCAlURoiAUIK6IrAZhBVofMSmADcYjEAHoAFS0iS08iBHqtZgQCDQ8IGUDZPQjJK6MAwRocrmMmAAAQeQWMkRAESRoF2Y1FnLiXRgaOMrKGEul-VlvUNxhq7RqtGYADMrQBLtHZGoy4wAC9RFAtqo96oo+rRUBKkBgQpF3q58p17SgjOpYgoAA8eiABjArULgINwKnmK1WgEIMAWD1M2AABSzeVhMg8difKTOiBkSg0Bg1zjcVG8PhiACUogr1OpMFYU2oSdakwMDYrvOgobFGuE+FSACsqFAAHQJFgexid9h8UsNnsbtGjMp7lgH8tfL6l03tgDaAGsKABPLhaYn6AC6fcEAgPqeFCskEZSlgABiIAAkIivm+pimBBn7fhQPZ1re8DIOg6y+Egj7KhQP4Vj2lJfP6gbzj6lKmEAA) 

Ce code est fonctionnel, cependant nous pouvons identifier deux problèmes potentiels:

1. Il n'y a aucune validation des clés des messages, rien ne nous empêche de demander la description d'un message `WELCOME` .
2. Il n'y a pas de validation sur les variables à interpoler. Si on décide dans une évolution future de rajouter des paramètres, ou si on oublie tout simplement de renseigner l'objet en question, les `{variables}` vont se retrouver telles quelles dans le message final. (Bonjour {prenom} {nom})

Ces deux problèmes sont certes vérifiables dans le code, au *runtime*, mais cela nous obligerait à traiter les cas d'erreur un peu partout.

Ce que l'on aimerait, c'est d'avoir une validation à la compilation des codes des messages, et des paramètres pour chaque message. De plus, cette validation nous facilitera les futurs développements, en nous proposant une autocomplétion de l'objet de paramètres.

![](https://lh3.googleusercontent.com/bKyqG-jJ9A7MEruKRGdAl64f4WSgj04zZZD92-fO5wBFcow0HlXqS9uWkI52iX1szoxjDYuHl4lM6D2QjA0FOiRBFVzJ1ZjkKvlKKLd03escNwchaqIJOYiqoVjvEe-5imeTNT_rsxo7_SdDduxKT8Q)

Exemple du code final voulu. La fonction `fillDescription` est capable de valider le code d'erreur, et on peut autocompléter l'objet `params`

## Validation de la clé du message

Avant de générer le type pour valider les paramètres du message, commençons par leur clé. Pour pouvoir valider le type des clés, nous allons avoir besoin de trois choses:

- `as const` sur l’objet DESCRIPTIONS nous permet d’indiquer à typescript que notre objet est une constante (dans le vrai sens du terme)
- `typeof DESCRIPTIONS`: puisque l’objet `DESCRIPTIONS` est une vraie constante, typescript est capable de dériver un type objet qui représente exactement notre objet
- `keyof typeof DESCRIPTIONS` permet d’extraire les clés du type précédent, ce qui nous donne le type `"HELLO" | "UPGRADE_SUBSCRIPTION" | "CANCEL_SUBSCRIPTION" | "SHARE_SOCIAL"` . Notons au passage que ce type est nécessaire à la fonction `fillDescription`. Si on essaye de remplacer ce type par string, on a une erreur lors de l’accès `DESCRIPTIONS[code]`. En effet, typescript n’est pas capable de garantir que toutes les chaînes de caractères sont valides.

```typescript
const DESCRIPTIONS = {
    HELLO: "Bienvenue {nom}",
    UPGRADE_SUBSCRIPTION: "Demande de changement d'abonnement {abonnementSource} vers {abonnementCible} à partir du {date}",
    CANCEL_SUBSCRIPTION: "Demande d'arrêt de l'abonnement {abonnement} à partir du {date}",
    SHARE_SOCIAL: "Partager l'article {titre} sur {reseauSocial}",
} as const;

type DescriptionsType = typeof DESCRIPTIONS
type DescriptionsCode = keyof DescriptionsType

/**
 * Remplissage de contenu d'un message
 * @param code le code du message à remplir
 * @param params les différents paramètres du message
 * @returns un message complet
 */
export function fillDescription(
    code: DescriptionsCode,
    params: Record<string, string>
) {
    // interpolate params
    const message = Object.entries<string>(params).reduce<string>(
        (msg, [key, value]) => msg.replace(`{${key}}`, value),
        DESCRIPTIONS[code]
    );
    return message;
}

```

Implémentation permettant la validation des codes des messages. [Lien vers le **playground* typescript](https://www.typescriptlang.org/play?ts=5.1.3&ref=sfeir.dev#code/MYewdgzgLgBAIgUQMoGEBKBJACgFQweQDkkYBeGAbwCgZaYAJBAGSfwC4YAiAIQEsBTMADdBAV36UwIALYBfTgBoadAKpYA4mgCCiAPpIV3VJlwFCHTnH7SAhmAAmExzGAALOwHNrg2PYDkNgBG4GDeYLAUQSFhUEggogBOwPyyMCIJEJRRYKHSPii8gQA2KTAABzAADjYJULwJMPailPY2UCmKyrQoWoQozPqGxth4RBZWtg5OAQkJAFe+EkUBwTkxWau5PqkV1bX1jc0Ure3ySnQwSPRaaAj6+CgYWkwWWDVQNl4Ny++8wCWUOpQBKlCCJSggiD8GyiOLAXg2IpnKipGyZUCQKAAbioVCgAE9KhIrBBgAleJU6uAIDhCRJyASiSAAGbwZDoEZmJB4unwfik8mU3jUlAgZzkADW-HxLL5AopVMgtKJuIA9AAqdU0dUwNDWSpFXgQCCfJwSDHtMDNfyiMAwPLG03amAAAT2NmkLjFS3N3sO9v5Jq85RgIOkBvqzrdNQ9VRj0kyJUy9l4zOZAEuQeFMu7pAAL4H8-0OoP8KMgqCJSAwW0Bx3B0DhkpQbWqqj8AAelRAtRgzNtwEVfd4RSKJLJCuFYAAFF0vY4OOPBYqIKLHOc6LmIBw9aAEvYADzQclgDwKGDH3ingB8VAAlJQ56rVTAr+0Et2im0JFu5xjoHWpZkDA+CBAAVvwg4AHQ+OS-JHsCV4eNe05bneUEgk0yQISeyGzhcFzTgmZ4wAA2lK+LnkIiLiAAug+pDXvaEAeBh-AGjYyTTgABhQAAkFAUbIsjcVRNH8HeG4EWywymEQSCkaAji0XOd44hcFZVoBpo4rIQA)

## Inférence de paramètres

Maintenant que nous avons validé statiquement nos clés de messages, passons au messages en eux mêmes.

Pour cela, nous allons besoin de faire des calculs au niveau type, donc pour cela nous allons définir un type paramétré (caractérisé par les chevrons: `ExtractParams<T>`).

Dans un premier temps, nous allons essayer de détecter les chaînes de caractères qui ne contiennent uniquement un `{placeholder}` . Pour cela nous allons tester si notre type `T` est une chaîne de caractère constituée de quelque chose entouré d’accolades. On laisse le compilateur inférer (deviner) le type de quelque chose. Si ce n’est pas le cas, on renvoie un type “objet vide”, sinon on peut construire un type objet avec comme clé notre valeur et en valeur le type `string`.

A noter que le type `Placeholder` inféré ne peut pas servir directement comme clé dans un type objet: le compilateur n’est pas capable d’assurer que l’on a des chaines uniques `"world"`, et non pas de chaines multiples `"hello" | "world"`, on est obligé de construire des clés à l’intérieur du type `K in Placeholder`.

```typescript
const SINGLE = "{world}" as const;

type ExtractParams<T> = T extends `{${infer Placeholder}}`
    ? { [K in Placeholder]: string }
    : {}

const extracted: ExtractParams<typeof SINGLE> = {
    world: ""
}

const num: ExtractParams<number> = {}
```

Parsing de chaine de caractère contenant uniquement une variable via l'inférence typescript. [Lien vers le **playground* typescript](https://www.typescriptlang.org/play?ts=5.1.3&ref=sfeir.dev#code/MYewdgzgLgBAygSQHIHEAyBRGBeGAiAbwHcQAnAGwBMBfPGAQwhlEigG4AoDqATwAcApjAwAPKKXrAoABXoSAthAA8AFQB8OGCpgCxAsJSYADAgBICASzAAzAaRjTykgQAsQVO9WpGOMPzAB+GAIYAG0AaRgrBydgV3dKOwBdAC4YaFIrAHMYal9-NII8jhZoHTEJKQFKNNFxSRk5ekUlXkEQa3hkdAwNXAJ8vxIKGvw8DmLS2DAAV3laiobZBWVZ+QAjOz7g6iA)

Nous avons notre première validation via l'inférence de type, mais on veut valider des messages entiers, pas seulement des messages contenant uniquement une clé. Pour cela, nous allons inférer deux parties dans le type, `Start` et `End`, et extraire récursivement nos paramètres sur le type `End`, et construire petit à petit notre type `Params` en fusionnant nos types objets. On notera au passage que l’on n’a pas besoin d’extraire les paramètres du type `Start`, puisqu’on construit l’objet du début vers la fin

```typescript
const SINGLE = "Bonjour {world} !" as const;

type ExtractParams<T> = T extends `${infer Start}{${infer Placeholder}}${infer End}`
    ? { [K in Placeholder]: string } & ExtractParams<End> // Remplacer string par { start: Start, end: End } pour voir les types inférés
    : {}

const extracted: ExtractParams<typeof SINGLE> = {
    world: ""
}

const num: ExtractParams<number> = {}

```

Parsing de chaine plus complexe via l'inférence typescript. [Lien vers le **playground* typescript](https://www.typescriptlang.org/play?ts=5.1.3&ref=sfeir.dev#code/MYewdgzgLgBAygSQHIHEAyBRGBeGAiAIXACsQBXAJxgG8B3ECgGwBMBfGAQjxgEMIZQkKAG4AUKKgBPAA4BTGBgAeUCj2BQACj1UBbCAB4AKgD4cMQzFnLZYZvwAGAEmoBLMADNZVOFG1RW1M5unlQajGqyABYgLF6srEEeXgq2rPaiMJkwAPw0MADaANIwbjBhEdGxFAC6AFww0BRuAOYw7ABkCsqq6lq6Bhi2pgD0wzAASrI60uHAyY0tMNLaedB+9T5+ADSWtvWDzG1L5FQAbiAuVIyy-FJy-MEAlxSPEBlZ9dSs4oLQlt1qKCyZj7AG9bQ8PT6O6yEDueDIdAYUy4ajvTL0Jgg-B4UTfUS-WBgMg6UEqQF9SEGYk6ABGXhRNFYQA)

## En résumé

Pour rassembler tout ça, nous avons: un moyen d’inférer les types des clés des messages, et un moyen d’inférer les paramètres du contenu des messages.

Il ne nous reste plus qu’à ajuster le type de `fillDescription` :

```typescript
/**
 * Remplissage de contenu d'un message
 * @param code le code du message à remplir
 * @param params les différents paramètres du message
 * @returns un message complet
 */
export function fillDescription<Code extends DescriptionsCode>(
    code: Code,
    params: ParseParams<DescriptionsType[Code]>
) {
    // interpolate params
    const message = Object.entries<string>(params).reduce<string>(
        (msg, [key, value]) => msg.replace(`{${key}}`, value),
        DESCRIPTIONS[code]
    );
    return message;
}

fillDescription("SHARE_SOCIAL", {
    titre: "",
    reseauSocial: ""
})
```

Implémentation finale, avec validation des clés et de l'objet paramètres. [Lien vers le **payground* typescript](https://www.typescriptlang.org/play?ts=5.1.3&ref=sfeir.dev#code/MYewdgzgLgBAIgUQMoGEBKBJACgFQweQDkkYBeGAbwCgZaYAJBAGSfwC4YAiAIQEsBTMADdBAV36UwIALYBfTgBoadAKpYA4mgCCiAPpIV3VJlwFCHTnH7SAhmAAmExzGAALOwHNrg2PYDkNgBG4GDeYLAUQSFhUEggogBOwPyyMCIJEJRRYKHSPii8gQA2KTAABzAADjYJULwJMPailPY2UCmKyrQoWoQozPqGxth4RBZWtg5OAQkJAFe+EkUBwTkxWau5PqkV1bX1jc0Ure3ySnQwSPRaaAj6+CgYWkwWWDVQNl4Ny++8wCWUOpQBKlCCJSggiD8GyiOLAXg2IpnKipGyZUCQKAAbioVCgAE9KhIrBBgAleJU6uAIDhCRJyASiSAAGbwZDoEZmJB4unwfik8mU3jUlAgZzkADW-HxLL5AopVMgtKJuMZEgQAA9gTZgFA3gkbNIIAAeHAAPjIMBwMH4WsE9kyAAMACQUXhgZn8BpID61WQUV3uz0NLBFHX8VwgIqOBKyWSBj1emAIByyR0wAD8lBgAG0ANIwd0wUPhyPRr0AXQ40HJYA8MFSADJk1qDbr9YaTSn7BaOBRZLiAPQAKmHNGHMG4RBUSA4KjqRV4BJgapXIBc4HSUHH2Zs1eB7vrTezgQ4YFE0kCSeP+rqiONFBcHGCUehYAbZp3UBAO8fe5gNaHgoMCnjA56Xl6wHABmz4gK+dgwAAPjAohTMy7r8PYDbjoOPJEsWIJQASxr4IEABWFrkI++aFu+UoyqypFkVWMBMfmFYNjAzb9jiVAjmOMATmg1iVIuEAQJ8TgSBi7Tno0fioTAeTiZJO4AAJ7IaG7OACoDOE0Sn8hJXjlDAILSKJ9TqZp0hVDUnYwCUmT2LwzLMgAlyC4SZDZAAXwL8ochkqV46mEYkkAoe+ynGdJMiifw26CbhtqVCAtQwMyqG6sK77oUURQkmSCq5caorOLaskOnKxVCiKYr8GaAAUXTafwHDlfw5x0DZEAcFghHEZq2rtvZRrGkVgqKjSdI5p1FZmp+ACUlCtYOg60e0CRpWG7R2QaRqtRi0DBbFlpMfwuoAHQ+OS-LGoBdbNb1S1XSCTTJA9B5PS1FwXE1RoeMBOb0cBQiIuIFYraQFqA29-CieGTWOgGFD0XGjpgxD-BLd1f1ssMphEEgOZ6fwFatUtOIXOFCTRUZkk4gOVD5YV-K1YqTWcFcNx3EgDxPEwiirRcQIghYnQ0-y0KwiA8KIhLKJLUAA)

## Bonus: rassembler une intersection de types

Vous aurez remarqué que le type inféré par `ExtractParams` est un peu difficile à lire:

```typescript
// Type inféré depuis fillDescription("SHARE_SOCIAL", {...})
function fillDescription<"SHARE_SOCIAL">(code: "SHARE_SOCIAL", params: {
    titre: string;
} & {
    reseauSocial: string;
}): string

```

Je vous propose ce petit type utilitaire pour rassembler ces différents types objets en un seul

```typescript
type Pretty<Obj> = { [K in keyof Obj]: Obj[K] } & {}

```

Nous avons vu ensemble comment extraire un type depuis un objet via une simple API. Nous aurions pu complexifier le modèle en y rajoutant un espèce de typage, en définissant par exemple des types dans le placeholder `{date|toDate}`, ou `{price|toEUR}` , mais cela dépasse la portée de cet article.

Dans ce playground final, je vous laisse avec un petit problème. Admettons que nous ayons une fonction `updateSubscription` qui a d’abord été codée pour annuler un abonnement, mais que l’on veut étendre à une évolution. On passe donc un booléen pour déterminer quel code utiliser, mais du coup le type inféré n’est pas suffisant: d’après le type de la clé `"UPGRADE_SUBSCRIPTION" | "CANCEL_SUBSCRIPTION"` , le type des paramètres est l’union des deux paramètres, mais rien ne nous empêche d’utiliser le premier type de paramètre avec le deuxième code.

Nous avons ici un problème de polymorphisme que nous ne pouvons pas résoudre simplement.

Une approche est de forcer l’intersection des deux objets paramètres, via le type `UnionToIntersection` de la bibliothèque [*utility-types*](https://github.com/piotrwitek/utility-types?ref=sfeir.dev).

Dans un prochain article, nous approfondirons les raisons pour lesquelles ce type est structuré de cette façon particulière et pourquoi il produit les résultats attendus.