L'objectif de cet article est de fournir un guide progressif de la gestion des types en typescript en se basant sur les type challenges. Nous présenterons chaque challenge, puis les différents concepts associés, et enfin une explication détaillée de la solution proposée.
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
L'objectif est d'implémenter le générique "Pick" déjà présent dans typescript. Il permet de créer un type à partir d'un sous-ensemble des propriétés d'un autre type.
- L'union de type
'title' | 'completed'
signifie que la valeur peut prendre l'un ou l'autre des types présentés. keyof
permet de générer une union de types autorisant chaque clé de l'objet.
interface Todo {
title: string
description: string
completed: boolean
}
type TodoKeys = keyof Todo;
// TodoKeys = 'title' | 'description' | 'completed'
- Typescript permet de créer des objets à partir d'un ensemble de clé. Exemple :
type Key = 'title' | 'completed'
type MyObj = {
[key in Key]: string
}
type MyObj2 = {
title: string;
completed: string;
}
Les 2 types MyObj
et MyObj2
sont Ă©quivalents.
- L'extraction de propriétés
interface Todo {
title: string
description: string
completed: boolean
}
type Title = Todo["title"];
// Title sera de type string
Dans un premier temps, nous limitons les valeurs possibles de la liste des clés demandées aux clés de l'objet Objet
.
type MyPick<Objet, K extends keyof Objet> = any
Ensuite, nous générons un objet à partir des clés fournies
type MyPick<Objet, K extends keyof Objet> = {
[key in K]: any
}
Il nous reste à typer la valeur des clés de notre objet
type MyPick<Objet, K extends keyof Objet> = {
[key in K]: Objet[key]
}
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
Il faut réimplémenter le générique Readonly<T>
déjà présent dans typescript.
Il empĂȘche de modifier l'ensemble des valeurs des clĂ©s de l'objet.
readonly
permet d'empĂȘcher la modification d'une clĂ© d'un objet
type ReadonlyTodo = {
readonly title: string
readonly description: string
}
Commençons par générer l'objet en tant que tel
type MyReadonly<T> = {
[key in keyof T]: T[key]
}
Il nous reste uniquement Ă passer chacune des valeurs en readonly :
type MyReadonly<T> = {
readonly [key in keyof T]: T[key]
}
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple>
// expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
as const
signifie que l'objet ne pourra plus ĂȘtre modifiĂ©. Dans le cadre de notre exemple :
const tesla: (typeof tuple)[0] = "tesla"
// ^? const tesla: "tesla"
Commençons par extraire les différentes valeurs de notre tuple.
type ValeursTuple<T extends Record<number, any>> = T[number]
const keys: ValeursTuple<typeof tuple> = "tesla"
// ^? "tesla" | "model 3" | "model X" | "model Y"
Ensuite, nous n'avons plus qu'à créer notre objet à partir de ces valeurs
type TupleToObject<T extends readonly any[]> = {
[key in T[number]]: key
}
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
Implémenter un généric qui retourne le type du premier élément d'un tableau.
- Les assertions de type : Typescript permet de faire des assertions sur des types et, suivant la véracité de l'assertion, retourner un type ou un autre.
type OnlyString<T> = T extends string ? T : never
type Ok = OnlyString<"str">
// type Ok = "str"
type Ko = OnlyString<{}>
// type Ko = never
à partir des assertions de type, l'implémentation est plutÎt directe
type First<T extends any[]> = T extends [] ? never : T[0];
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
type Length<T extends readonly any[]> = T["length"]
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
Créer un générique permettant de retirer un pour plusieurs types d'une union de types.
type MyExclude<T, U> = T extends U ? never : T
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
Créer un générique permettant de récupérer le type d'une promise
infer
permet de récupérer le type dans une assertion.
type Example<T extends Array<any>> = T extends Array<infer R> ? R : never
type Str = Example<string[]>
// Str = string
PromiseLike
est une implémentation minimaliste de la promise. C'est simplement un objet ayant une méthodethen()
valide pour une promise.
Dans un premier temps, nous gérons le cas de promesse non "nested".
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer R> ?
R :
never;
Dans le cas ou R
est une promesse, nous voulons rĂ©cupĂ©rer son rĂ©sultat. Il nous suffit d'ajouter un appel rĂ©cursif Ă
MyAwaited si R
est de type PromiseLike
.
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer R> ?
R extends PromiseLike<any> ?
MyAwaited<R> :
R:
never;
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
Créer un générique If
retournant le deuxiĂšme argument si le premier est true
, et le 3Ăšme sinon
Implémentation plutÎt directe, RAS.
type If<C extends boolean, T, F> = C extends true ? T : F;
type Result = Concat<[1], [2]> // expected to be [1, 2]
Créer un générique Concat
retournant la concaténation des 2 tableaux en paramÚtre
- Array Spread. Typescript permet de
spread
des tuples. Par exemple, pour ajouter un élément à la fin d'un tuple :
type AddElementToTuple<T extends any[], U> = [...T, U];
Implémentation plutÎt directe, RAS.
type Concat<T extends any[], U extends any[]> = [...T, ...U];
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
Créer un générique Includes
déterminant si un tuple contient un type donné.
- Vous pouvez utiliser des génériques dans les opérateurs ternaires de types. Exemple :
type assertion = Equal<T1, T2> extends true ? "egaux" : "non-Ă©gaux"
GĂ©rons d'abord le cas oĂč le premier Ă©lĂ©ment est du bon type :
type Includes<T extends readonly any[], U> = T extends readonly [infer Head, ...infer Tail] ?
Equal<Head, U> :
false
Si le premier élement n'est pas du bon type, il faut faire un appel récursif à Includes
sur le reste du tuple. Pour
faire notre ternaire, nous utilisons : Equal<Head, U> extends true
type Includes<T extends readonly any[], U> = T extends readonly [infer Head, ...infer Tail] ?
Equal<Head, U> extends true ?
true:
Includes<Tail, U> :
false
type Result = Push<[1, 2], '3'> // [1, 2, '3']
Créer un générique pour ajouter un élément à la fin d'un tuple
En utilisant le spread operator, rien de bien compliqué
type Push<T extends any[], U> = [...T, U]
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
Créer un générique pour ajouter un élément au début d'un tuple
En utilisant le spread operator, rien de bien compliqué
type Unshift<T extends any[], U> = [U, ...T]
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
Créer un générique permettant d'extraire le type des paramÚtres d'une fonction
- L'opérateur
infer
peut aussi ĂȘtre utilisĂ© sur les arguments d'une fonction.
En sachant que infer
peut ĂȘtre utilisĂ© sur les arguments d'une fonction, l'implĂ©mentation est plutĂŽt directe.
type MyParameters<T extends (...args: any[]) => any> =
T extends (...args: infer Args) => any ?
Args :
never;
const fn = (v: boolean) => {
if (v)
return 1
else
return 2
}
type a = MyReturnType<typeof fn> // should be "1 | 2"
Implémenter le générique ReturnType
de typescript qui récupÚre le type de retour d'une fonction.
- L'opérateur
infer
peut aussi ĂȘtre utilisĂ© sur le type de retour d'une fonction
En sachant que infer
peut ĂȘtre utilisĂ© sur le type de retour d'une fonction, l'implĂ©mentation est plutĂŽt directe.
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer Return ?
Return :
never
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyOmit<Todo, 'description' | 'title'>
const todo: TodoPreview = {
completed: false,
}
Implémenter le générique Omit
de typescript qui retire des propriétés d'un objet.
- Le générique
Exclude
est built-in dans Typescript
Il nous suffit de retirer les clĂ©s devant ĂȘtre ignorĂ©es grĂące Ă Exclude
type MyOmit<T, K extends keyof T> = {
[Key in Exclude<keyof T, K>]: T[Key]
}
interface Todo {
title: string
description: string
completed: boolean
}
const todo: MyReadonly2<Todo, 'title' | 'description'> = {
title: "Hey",
description: "foobar",
completed: false,
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
todo.completed = true // OK
Implémenter un générique MyReadonly2<T, K>
qui permet de transformer les propriétés K
de T
en readonly.
- Les génériques
Pick
etOmit
sont built-in dans Typescript
Créons un objet contenant les propriétés à marquer en readonly
type MyReadonly2<T, K extends keyof T> = {
readonly [Key in K]: T[Key]
}
Lorsque le 2Úme paramÚtre n'est pas fourni, toutes les valeurs passent en readonly. Mettons lui une valeur par défaut.
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [Key in K]: T[Key]
}
Ajoutons ensuite les éléments qui ne seront pas en readonly
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [Key in K]: T[Key]
} & {
[Key in Exclude<keyof T, K>]: T[Key]
}
Il semble y avoir un problĂšme sur le dernier test.
type T = MyReadonly2<Todo2, 'description'>
/*
type T = {
readonly description?: string | undefined;
} & {
title: string;
completed: boolean;
}
*/
Or nous voulons :
interface Expected {
readonly title: string
readonly description?: string
completed: boolean
}
Le readonly du title a disparu. Pour le conserver, nous avons l'astuce suivante. En effet,
lorsque le keyof
est accolé au in
, le modificateur readonly
est conservé.
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [Key in K]: T[Key]
} & {
[Key in keyof Omit<T, K>]: T[Key]
}