Skip to content

An in-depth explanation of type challenges

Notifications You must be signed in to change notification settings

cberthou/type-chalenges-article

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 

Repository files navigation

Type challenges

Introduction

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.

Les challenges "easy"

Présentation

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.

Concepts

  • 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

Implémentation

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

Présentation

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.

Concepts

  • readonly permet d'empĂȘcher la modification d'une clĂ© d'un objet
type ReadonlyTodo = {
    readonly title: string
    readonly description: string
}

Implémentation

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

Présentation

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

Concepts

  • 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"

Implémentation

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
}

Présentation

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.

Concepts

  • 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

Implémentation

À partir des assertions de type, l'implĂ©mentation est plutĂŽt directe

type First<T extends any[]> = T extends [] ? never : T[0];

Présentation

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

Implémentation

type Length<T extends readonly any[]> = T["length"]

Présentation

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.

Implémentation

type MyExclude<T, U> = T extends U ? never : T

Présentation

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

Concepts

  • 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Ă©thode then() valide pour une promise.

Implémentation

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;

Présentation

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

Implémentation plutÎt directe, RAS.

type If<C extends boolean, T, F> = C extends true ? T : F;

Présentation

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

Concepts

  • 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

Implémentation plutÎt directe, RAS.

type Concat<T extends any[], U extends any[]> = [...T, ...U];

Présentation

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é.

Concepts

  • 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"

Implémentation

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

Présentation

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

Implémentation

En utilisant le spread operator, rien de bien compliqué

type Push<T extends any[], U> = [...T, U]

Présentation

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

Implémentation

En utilisant le spread operator, rien de bien compliqué

type Unshift<T extends any[], U> = [U, ...T]

Présentation

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

Concepts

  • L'opĂ©rateur infer peut aussi ĂȘtre utilisĂ© sur les arguments d'une fonction.

Implémentation

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;

Les challenges "medium"

Présentation

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.

Concepts

  • L'opĂ©rateur infer peut aussi ĂȘtre utilisĂ© sur le type de retour d'une fonction

Implémentation

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

Présentation

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.

Concepts

  • Le gĂ©nĂ©rique Exclude est built-in dans Typescript

Implémentation

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

Présentation

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.

Concepts

  • Les gĂ©nĂ©riques Pick et Omit sont built-in dans Typescript

Implémentation

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

About

An in-depth explanation of type challenges

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published