Comportementaux
Iterator
Parcourir une collection sans exposer sa structure interne
Problème
On veut parcourir les éléments d'une collection (liste, arbre, graphe, pagination) sans exposer sa structure interne ni créer de couplage entre le client et l'implémentation.
Iterator natif en TypeScript
TypeScript/JavaScript dispose d'un protocole Iterator natif via Symbol.iterator. Tout objet qui l'implémente est itérable avec for...of.
class Range {
constructor(private start: number, private end: number, private step = 1) {}
[Symbol.iterator](): Iterator<number> {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next(): IteratorResult<number> {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined as never, done: true };
},
};
}
}
const range = new Range(1, 10, 2);
for (const n of range) {
console.log(n); // 1, 3, 5, 7, 9
}
// Utilisable avec spread et destructuring
const numbers = [...new Range(1, 5)]; // [1, 2, 3, 4, 5]Générateurs comme Iterator
Les générateurs simplifient l'implémentation des iterators :
function* range(start: number, end: number, step = 1): Generator<number> {
for (let i = start; i <= end; i += step) {
yield i;
}
}
function* paginate<T>(items: T[], pageSize: number): Generator<T[]> {
for (let i = 0; i < items.length; i += pageSize) {
yield items.slice(i, i + pageSize);
}
}
for (const page of paginate(products, 10)) {
await renderPage(page);
}Iterator pour structures arborescentes
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
function* depthFirst<T>(node: TreeNode<T>): Generator<T> {
yield node.value;
for (const child of node.children) {
yield* depthFirst(child);
}
}
function* breadthFirst<T>(root: TreeNode<T>): Generator<T> {
const queue: TreeNode<T>[] = [root];
while (queue.length > 0) {
const node = queue.shift()!;
yield node.value;
queue.push(...node.children);
}
}
// Même interface, traversal différent
const menuTree: TreeNode<MenuItem> = { /* ... */ };
for (const item of depthFirst(menuTree)) {
console.log(item.label);
}Pagination lazy avec Iterator
async function* fetchAllPages<T>(
fetcher: (page: number) => Promise<{ data: T[]; hasMore: boolean }>
): AsyncGenerator<T> {
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await fetcher(page);
for (const item of result.data) {
yield item;
}
hasMore = result.hasMore;
page++;
}
}
// Traitement en streaming, sans tout charger en mémoire
const allProducts = fetchAllPages((page) =>
fetch(`/api/products?page=${page}`).then(r => r.json())
);
for await (const product of allProducts) {
await indexProduct(product);
}En React : listes infinies
function useInfiniteProducts(category: string) {
return useInfiniteQuery({
queryKey: ['products', category],
queryFn: ({ pageParam = 1 }) =>
fetch(`/api/products?category=${category}&page=${pageParam}`).then(r => r.json()),
getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.currentPage + 1 : undefined,
});
}
function ProductList({ category }: { category: string }) {
const { data, fetchNextPage, hasNextPage } = useInfiniteProducts(category);
// TanStack Query itère les pages en interne
const products = data?.pages.flatMap(page => page.data) ?? [];
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>Charger plus</button>
)}
</div>
);
}