MFormations
Modern Frontend Engineering

Chapitre 6

Chapitre 06 — TypeScript

Chapitre 06 — TypeScript

Chapitre 06 — TypeScript : Cours Complet

1. Histoire et Motivation

1.1 Le problème JavaScript

JavaScript a été créé en 10 jours par Brendan Eich en 1995 pour Netscape Navigator. Conçu à l'origine pour de petites interactions dans le navigateur, il n'a jamais été pensé pour des applications de la taille de VS Code, Figma ou Teams.

Problèmes fondamentaux de JavaScript pour les grandes codebases :

  • Absence de typage statique → les erreurs de type ne sont découvertes qu'à l'exécution
  • Documentation implicite : impossible de savoir ce qu'une fonction attend sans lire son implémentation
  • Refactoring risqué : renommer une propriété peut casser des centaines de fichiers sans alerte
  • Outillage limité : autocomplétion et navigation de code imprécises

1.2 La réponse Microsoft

En 2012, Microsoft dévoile TypeScript, conçu par Anders Hejlsberg. L'objectif n'est pas de remplacer JavaScript mais de l'étendre :

JavaScript + Types = TypeScript

Principes fondateurs :

  • TypeScript est un sur-ensemble de JavaScript (tout JS valide est TS valide)
  • Le typage est optionnel et progressif
  • Les types sont effacés à la compilation (pas de runtime overhead)
  • La compilation produit du JavaScript lisible et standard
  • L'inférence de types réduit la verbosité

1.3 Adoption industrielle

TypeScript est aujourd'hui adopté par :

  • Google (Angular écrit en TS, utilisé en interne)
  • Microsoft (VS Code, Teams, Azure SDK)
  • Meta (React, Jest, Flow migré vers TS)
  • Airbnb, Shopify, Netflix, Asana, et des milliers d'autres

Selon le State of JS Survey 2024, TypeScript est utilisé par plus de 85% des développeurs JavaScript.

2. Types Fondamentaux

2.1 Types primitifs

let name: string = "TypeScript";
let age: number = 30;
let isReady: boolean = true;
let data: null = null;
let value: undefined = undefined;
let big: bigint = 100n;
let sym: symbol = Symbol("unique");
let anything: any = "peut être n'importe quoi";
let unknown: unknown = "doit être vérifié avant usage";
let nothing: void = undefined;
let never: never; // n'arrive jamais (ex: fonction qui throw)

2.2 Type Inference

TypeScript infère les types automatiquement :

let count = 42; // inféré: number
const name = "TS"; // inféré: "TS" (literal type)
let items = [1, 2, 3]; // inféré: number[]

2.3 Unions et Intersections

Union types : une valeur peut être de type A OU B

type Status = "idle" | "loading" | "success" | "error";
type Result<T> = T | Error;
type ID = string | number;

function printID(id: ID) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id.toFixed(2));
  }
}

Intersection types : une valeur combine les propriétés de A ET B

type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
// { name: string; age: number; }

2.4 Tuples

type Point = [number, number];
type RGB = [red: number, green: number, blue: number];
type HTTPResponse = [status: number, data: unknown];

const origin: Point = [0, 0];
const color: RGB = [255, 128, 0];

2.5 Enums

enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

enum HttpStatus {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500
}

// Const enum (zéro runtime cost)
const enum Color {
  Red = "#FF0000",
  Green = "#00FF00",
  Blue = "#0000FF"
}

3. Interfaces vs Types

3.1 Interfaces

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user";
  readonly createdAt: Date;
  updatedAt?: Date;
}

// Declaration merging (uniquement avec interface)
interface User {
  lastLogin: Date;
}
// User a maintenant 6 propriétés

3.2 Type Aliases

type User = {
  id: string;
  name: string;
  email: string;
};

// Union types (impossible avec interface)
type Result = User | Error;

// Mapped types
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

3.3 Quand utiliser quoi ?

CritèreInterfaceType
Declaration merging
Classes (implements)
Union types
Mapped types
Performance✅ (plus rapide)⚠️ (plus lent avec des unions larges)
Héritageextends& (intersection)

Règle empirique : utilisez interface pour les objets/classes de l'API publique, type pour les unions, utilitaires et transformations.

4. Generics

4.1 Syntaxe de base

function identity<T>(arg: T): T {
  return arg;
}

const num = identity(42); // T inféré: number
const str = identity("hello"); // T inféré: string

4.2 Contraintes

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello"); // OK: string a length
logLength([1, 2, 3]); // OK: Array a length
logLength(42); // Error: number n'a pas length

4.3 Generic Constraints avancées

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // OK: string
getProperty(user, "age"); // OK: number
getProperty(user, "unknown"); // Error

4.4 Conditional Types

type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false

// Conditional type avec inference (infer)
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string

// Distributive conditional types
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]

4.5 Infer

type ExtractPromise<T> = T extends Promise<infer U> ? U : T;
type A = ExtractPromise<Promise<string>>; // string
type B = ExtractPromise<number>; // number

type FirstArgument<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
type Fn = (x: number, y: string) => boolean;
type Arg = FirstArgument<Fn>; // number

5. Utility Types

5.1 Built-in Utility Types

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
}

// Partial: toutes les propriétés deviennent optionnelles
type PartialTodo = Partial<Todo>;

// Required: toutes les propriétés deviennent requises
type RequiredTodo = Required<PartialTodo>;

// Pick: sélectionner certaines propriétés
type TodoPreview = Pick<Todo, "title" | "completed">;

// Omit: exclure certaines propriétés
type TodoWithoutDate = Omit<Todo, "createdAt">;

// Record: dictionnaire typé
type PageInfo = Record<string, { title: string; url: string }>;

// Exclude / Extract: unions
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"

// NonNullable
type T2 = NonNullable<string | number | null | undefined>; // string | number

// Parameters / ReturnType
type Fn = (x: number, y: string) => boolean;
type Params = Parameters<Fn>; // [number, string]
type Ret = ReturnType<Fn>; // boolean

// Awaited
type PromiseResult = Awaited<Promise<Promise<string>>>; // string

5.2 Mapped Types

// Rendre toutes les propriétés readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Rendre toutes les propriétés optionnelles
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Modifier le type des propriétés
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Avec remapping de clés (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

5.3 Template Literal Types

type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

type CSSValue = number | string;
type CSSProperty = "margin" | "padding" | "border";
type CSSDeclaration = `${CSSProperty}-${"top" | "right" | "bottom" | "left"}`;
// "margin-top" | "margin-right" | ... | "border-left"

// Parsing avec infer
type ParseRoute<T extends string> =
  T extends `${infer Base}/:${infer Param}`
    ? { base: Base; param: Param }
    : never;

type Route = ParseRoute<"/users/:id">;
// { base: "/users"; param: "id" }

6. Type Guards

6.1 typeof

function format(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // TS sait que c'est string
  }
  return value.toFixed(2); // TS sait que c'est number
}

6.2 instanceof

class ApiError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
  }
}

function handleError(error: Error | ApiError) {
  if (error instanceof ApiError) {
    console.log(`API Error ${error.statusCode}: ${error.message}`);
  } else {
    console.log(`Generic Error: ${error.message}`);
  }
}

6.3 Custom Type Predicates

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // OK: type narrowé vers Fish
  } else {
    pet.fly(); // OK: type narrowé vers Bird
  }
}

6.4 Discriminated Unions

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

7. Declaration Files (.d.ts)

7.1 Structure d'un .d.ts

// math.d.ts
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export const PI: 3.14159;
export interface Calculator {
  add(a: number, b: number): number;
  memory: number;
}

7.2 Ambient Declarations

// Pour déclarer un module sans typages
declare module "*.css" {
  const classes: { [key: string]: string };
  export default classes;
}

declare module "*.svg" {
  const content: string;
  export default content;
}

// Déclarer une variable globale
declare const __VERSION__: string;
declare function trackEvent(name: string, data?: Record<string, unknown>): void;

7.3 Augmentation de module

// express.d.ts
import "express";

declare module "express" {
  interface Request {
    user?: {
      id: string;
      role: "admin" | "user";
    };
  }
}

8. Configuration tsconfig.json

8.1 Configuration strict minimale

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

8.2 Strict Mode expliqué

// strict: true active ces 7 flags :
// - strictNullChecks: null/undefined ne sont pas assignables à tout type
// - noImplicitAny: interdit les `any` implicites
// - strictFunctionTypes: variance checking sur les fonctions
// - strictBindCallApply: bind/call/apply correctement typés
// - strictPropertyInitialization: propriétés doivent être initialisées
// - noImplicitThis: interdit le this implicite
// - alwaysStrict: strict mode JS dans chaque fichier

8.3 Module Resolution

// Classic (legacy) vs Node (pour Node.js) vs Bundler (pour Vite/Webpack)
// NodeNext: respecte la résolution ESM de Node.js

// moduleResolution: "bundler" — le plus flexible pour projets modernes
// Permet d'omettre les extensions .js dans les imports

9. TypeScript avec React

9.1 Typage des Props

interface ButtonProps {
  label: string;
  variant?: "primary" | "secondary" | "danger";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  children?: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({
  label,
  variant = "primary",
  size = "md",
  disabled = false,
  onClick,
  children
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
      {children}
    </button>
  );
};

9.2 Typage des Hooks

// useState avec inférence
const [count, setCount] = useState(0); // type: number
const [user, setUser] = useState<User | null>(null);

// useReducer typé
type Action =
  | { type: "INCREMENT"; payload: number }
  | { type: "DECREMENT" }
  | { type: "RESET" };

interface State {
  count: number;
  lastAction: string;
}

const initialState: State = { count: 0, lastAction: "INIT" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + action.payload, lastAction: "INCREMENT" };
    case "DECREMENT":
      return { count: state.count - 1, lastAction: "DECREMENT" };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}

// useRef
const inputRef = useRef<HTMLInputElement>(null);
const intervalRef = useRef<number | null>(null);

// useMemo / useCallback avec typage implicite
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

const handleClick = useCallback((e: React.MouseEvent) => {
  console.log(e.clientX, e.clientY);
}, []);

9.3 Typage des Events

// onChange pour input
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

// onSubmit pour formulaire
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // traitement
};

// onDrag
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
  e.dataTransfer.setData("text/plain", "data");
};

10. Bonnes Pratiques et Patterns Avancés

10.1 Avoid any

// ❌ À éviter
function process(data: any) {
  return data.value;
}

// ✅ Préférer unknown
function process(data: unknown) {
  if (data && typeof data === "object" && "value" in data) {
    return (data as { value: unknown }).value;
  }
  throw new Error("Invalid data");
}

10.2 Branded Types

type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const userId = "abc" as UserId;
const orderId = "xyz" as OrderId;

getUser(userId); // OK
getUser(orderId); // Error: Type '"OrderId"' is not assignable

10.3 Builder Pattern avec Types

class QueryBuilder<T extends Record<string, unknown>> {
  private conditions: Partial<T> = {};

  where<K extends keyof T>(key: K, value: T[K]): this {
    this.conditions[key] = value;
    return this;
  }

  build(): Partial<T> {
    return { ...this.conditions };
  }
}

interface UserQuery {
  name: string;
  age: number;
  email: string;
}

const query = new QueryBuilder<UserQuery>()
  .where("name", "Alice")
  .where("age", 30)
  .build();

10.4 Flatten / DeepPartial

type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

type DeepReadonly<T> = T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

interface Config {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
}

type PartialConfig = DeepPartial<Config>;
// tout devient optionnel récursivement

10.5 Type-safe Event Emitter

type EventMap = {
  userCreated: { id: string; name: string };
  userDeleted: { id: string };
  error: { message: string; code: number };
};

class TypedEmitter<T extends Record<string, unknown>> {
  private listeners: Map<keyof T, Set<(...args: any[]) => void>> = new Map();

  on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }
}

const emitter = new TypedEmitter<EventMap>();
emitter.on("userCreated", (data) => {
  console.log(data.name); // typé!
});

11. Comparaison : TypeScript vs Flow vs JSDoc

CritèreTypeScriptFlow (Meta)JSDoc
Performance compilation⚡ Rapide (tsserver)⚠️ Lent✅ Aucune
Écosystème🌟 Excellent⚠️ Limité✅ Universel
Adoption85%+ des devs< 5%~30% (VS Code)
StrictnessConfigurableStrict par défautLimité
IDE SupportExcellent (LSP)CorrectBon (VS Code)
FonctionnalitésCompletesPartiellesBasiques
MigrationCompilateurChecker uniquementAucune

12. Erreurs Courantes des Juniors

// ❌ any partout
function fetchData(): any { /* ... */ }

// ✅ Typer correctement
function fetchData(): Promise<ApiResponse> { /* ... */ }

// ❌ Surcharger inutilement
const items: string[] = ["a", "b"];

// ✅ Laisser l'inférence faire son travail
const items = ["a", "b"];

// ❌ as partout
const user = data as User;

// ✅ Type narrowing
if (isUser(data)) { /* ... */ }

// ❌ Ignorer strictNullChecks
function getName(user: User | null) {
  return user.name; // 💥 Runtime error
}

// ✅ Vérifier null
function getName(user: User | null) {
  return user?.name ?? "Anonymous";
}

13. Ressources