Implement IntersectionObserver in TypeScript for lazy-loading images

Tests precise DOM typing and observer lifecycle. A strong answer types the callback as receiving IntersectionObserverEntry[], narrows entry.target to HTMLImageElement, swaps data-src to src, and calls unobserve.
WHAT THIS TESTS: This question tests three senior-level skills: precise TypeScript typing of browser APIs, understanding of structural DOM types, and memory-conscious lifecycle management. The interviewer wants to see that you know IntersectionObserverEntry is the correct type for the callback payload, that entry.target is typed as Element rather than HTMLElement, and that you understand why explicit typing matters for maintainability and refactoring. They are also checking whether you know the difference between the observer instance and the entry objects it emits.
A GOOD ANSWER COVERS: First, declare the callback with the exact signature the browser expects: a function taking entries as IntersectionObserverEntry[] and the observer instance as IntersectionObserver, returning void. Second, inside the callback, loop over entries and check entry.isIntersecting before acting. Third, narrow entry.target from Element to HTMLImageElement using instanceof or a type guard before touching image-specific properties like src or dataset.src. Fourth, after swapping the data-src attribute into src, call observer.unobserve(entry.target) so the observer stops watching that node and frees up resources. Fifth, optionally store the observer in a ref or variable so you can call disconnect when the component unmounts or the page changes. Mentioning that root and rootMargin are also configurable properties on the observer options object shows deeper familiarity with the API.
COMMON WRONG ANSWERS: A major red flag is typing the callback arguments as any or relying entirely on implicit any, which erases safety on entry.target. Another mistake is assuming entry.target is already an HTMLElement or HTMLImageElement without narrowing, because TypeScript will error if you try to assign to src on a generic Element. Some candidates forget to call unobserve after the image loads, which keeps the observer alive and can trigger unnecessary callbacks. Finally, typing entries as a single IntersectionObserverEntry instead of an array shows confusion about the API contract. Equally bad is creating a new observer for every image rather than reusing one instance to watch multiple targets.
LIKELY FOLLOW-UPS: The interviewer may ask how you would handle responsive images with srcset and sizes, which requires typing the target as HTMLImageElement and mutating those specific properties safely. They might ask about rootMargin for preloading images before they enter the viewport, or how to polyfill or gracefully degrade for older browsers. Another follow-up is testing performance: why not use scroll events? The answer is that IntersectionObserver runs off the main thread and avoids layout thrashing. You might also be asked how to test this in a headless environment where layout calculations behave differently.
ONE CONCRETE EXAMPLE: const imgObserver = new IntersectionObserver((entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target as HTMLImageElement; if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute('data-src'); observer.unobserve(img); } } }); }, { rootMargin: '50px', threshold: 0.01 }); document.querySelectorAll('img[data-src]').forEach(img => imgObserver.observe(img));
Source: developer.mozilla.org
Read the original → developer.mozilla.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.