Dans les projets Sockeet.io, l'absence de typage strict des événements entraîne fréquemment des bugs en production : noms d'événements mal orthographiés, structures de données incohérentes entre client et serveur, ou encore perte d'autocomlpétion dans l'IDE. Type-Fest propose des utilitaires de type qui permettent d'éliminer ces problèmes à la compilation.
Problématique du typage dans Socket.io
Une implémentation non typée ressemble généralement à ceci :
const client = io();
client.on('user-connected', (payload) => {
// payload est de type 'any' — aucune vérification
console.log(payload.userName); // faute de frappe non détectée
});
client.emit('new-message', {
content: 'Salut',
createdAt: new Date() // le serveur attend un timestamp numérique
});
Cette approche induit trois risques majeurs : erreurs de frappe sur les noms d'événements, inadéquation entre les données envoyées et attendues, et dérive entre la documentation et le code.
Outils de type essentiels
TaggedUnion pour les contrats d'événements
TaggedUnion génère une union discriminée à partir d'un objet de correspondances nom-événement / type-de-données :
import type { TaggedUnion } from 'type-fest';
type EventContract = TaggedUnion<{
'member-connected': { memberId: string; displayName: string };
'chat-sent': { body: string; sentAt: number };
'member-disconnected': string;
}>;
Cette construction lie chaque nom d'événement à son type de données associé de manière statique.
UnionToTuple pour l'extraction des noms
UnionToTuple transforme une union en tuple, utile pour contraindre les paramètres de fonction :
import type { UnionToTuple } from 'type-fest';
type KnownEvents = UnionToTuple<EventContract['type']>;
// ['member-connected', 'chat-sent', 'member-disconnected']
function assertValidEvent<E extends KnownEvents[number]>(evt: E): E {
return evt;
}
assertValidEvent('member-connected'); // valide
assertValidEvent('unknown-evt'); // erreur de compilation
Extraction des types de données avec Extract
En combinant Extract et les utilitaires de Type-Fest, on obtient un type de gestionnaire entièrement sûr :
type SafeListener = <E extends EventContract['type']>(
evt: E,
cb: (payload: Extract<EventContract, { type: E }>['data']) => void
) => void;
const register: SafeListener = (evt, cb) => {
client.on(evt, cb);
};
register('member-connected', (info) => {
// info est inféré comme { memberId: string; displayName: string }
console.log(info.memberId);
});
Mise en œuvre d'un client Socket.io typé
Étape 1 — Définir les contrats
// file: src/contracts/socket-contracts.ts
import type { TaggedUnion } from 'type-fest';
export type ServerToClient = TaggedUnion<{
'member-joined': { uid: string; label: string };
'incoming-message': { msgId: string; body: string; author: string };
}>;
export type ClientToServer = TaggedUnion<{
'enter-room': { roomKey: string };
'post-message': { body: string; roomKey: string };
}>;
Étape 2 — Créer le wrapper typé
// file: src/clients/safe-socket.ts
import { io, type Socket } from 'socket.io-client';
import type { UnionToTuple } from 'type-fest';
import type { ClientToServer, ServerToClient } from '../contracts/socket-contracts';
type OutgoingEvents = UnionToTuple<ClientToServer['type']>;
export function buildSafeSocket() {
const conn = io() as Socket<ServerToClient, ClientToServer>;
function safeEmit<E extends OutgoingEvents[number]>(
evt: E,
payload: Extract<ClientToServer, { type: E }>['data']
) {
conn.emit(evt, payload);
}
return { ...conn, emit: safeEmit };
}
Étape 3 — Utilisation dans les composants
import { buildSafeSocket } from './clients/safe-socket';
const sock = buildSafeSocket();
sock.on('member-joined', (member) => {
console.log(`${member.label} est connecté`);
});
sock.emit('post-message', {
body: 'Bonjour',
roomKey: 'room-42'
});
sock.emit('post-message', {
body: 'Erreur'
// roomKey manquant → erreur de compilation
});
Bénéfices concrets
Avec cette architecture, l'IDE propose l'autocomplétion des noms d'événements et valide les structures de données à la saisie. Lors d'un renommage ou d'un ajout de champ, le compilateur signale immédiatement tous les sites d'utilisation à mettre à jour.
// Ajout d'un champ optionnel
type ServerToClient = TaggedUnion<{
'member-joined': {
uid: string;
label: string;
avatarUrl?: string;
};
}>;
Techniques avancées
Gestion des données partielles
import type { PartialDeep } from 'type-fest';
type MessagePayload = {
msgId: string;
body: string;
author: { uid: string; label: string };
};
type PatchableMessage = PartialDeep<MessagePayload>;
// Toutes les propriétés imbriquées deviennent optionnelles
Typage des middlewares
import type { Parameters, ReturnType } from 'type-fest/source/internal/type';
type Hook<F extends (...a: any[]) => any> = (
...args: Parameters<F>
) => ReturnType<F> | Promise<ReturnType<F>>;
const loggingHook: Hook<typeof sock.emit> = (evt, payload) => {
console.debug(`[emit] ${evt}`, payload);
return sock.emit(evt, payload);
};
Cette métohdologie s'étend naturellement à tout système piloté par événements : WebSockets natifs, EventEmitter Node.js, ou architectures pub/sub.