Study Guide [Front-End]
High priority (70% of your time)
- Fleet data manipulation โ
filter,sort,reduce,Map - UI component builds โ filterable list, debounced search, tree view
- DOM manipulation โ
createElement,addEventListener, render from data - Light BFS/DFS โ level order, grid traversal, simple tree recursion
- Narrating Big-O out loud for every solution
Low priority โ skip for this role
- Hard graph problems (clone graph, topological sort)
- Backtracking โ subsets, permutations, combination sum
- GFE polyfills track (debounce/curry/deep clone) โ useful but not Waymo-specific
- Heap / priority queue
- Dynamic programming
Example problem they could give
"You have an array of vehicle objects: { id, status, stoppedSeconds, zoneId }. Build a UI that renders the list, lets the user filter by status via a dropdown, and sorts by stoppedSeconds descending. Add a debounced text input that filters by id."
This hits every bullet on the TPS guide: arrays/maps, sorting/filtering, UI implementation, and you'd state O(n log n) for the sort. Do this build before the interview.
Confirm with the recruiter
Ask directly: which round tests UI-building vs. DSA-in-JS (or both, per the guide), what coding environment they use (CoderPad, CodeSandbox, shared doc), and whether vanilla JS is genuinely fine or if they lean toward seeing a framework.
1. No code formatter for JSX/TSX. "Format Document" throws "no formatter for typescriptreact/javascriptreact", and renaming
.tsx โ .jsx โ .js does NOT fix it โ any file containing JSX is a React language mode with no Prettier wired in. Don't burn interview time chasing it. Keep prettier.io/playground open in a tab as a paste-in/paste-back fallback, or just indent cleanly as you type (Monaco auto-indents on newlines and braces). Interviewers don't dock you for imperfect spacing.
2. Type squiggles don't block running. The pad runs via Vite/esbuild, which STRIPS types without type-checking, so red TS squiggles are cosmetic โ the preview still renders. To write plain untyped JS in a
.tsx file with zero noise, put // @ts-nocheck as the first line of the file. Alternatives: use a .jsx file, or add : any to the few flagged params. TS also infers most types, so only untyped function params (e.g. destructured props) tend to complain.
JavaScript Patterns Cheatsheet
// Arrays const arr = [1, 2, 3]; arr.push(4); // add to end โ O(1) arr.pop(); // remove from end โ O(1) arr.shift(); // remove from front โ O(n) โ ๏ธ not O(1) like Python deque arr.unshift(0); // add to front โ O(n) arr.slice(1, 3); // [arr[1], arr[2]] โ non-mutating, end-exclusive arr.splice(1, 2); // removes 2 elements starting at index 1 โ mutates! arr.length; arr[arr.length - 1]; // last element (no arr[-1] in JS) [...arr].reverse(); // reversed copy; arr.reverse() mutates in-place // Sets โ O(1) average membership const s = new Set([1, 2, 3]); s.add(4); s.delete(2); s.has(3); // true s.size; // 3 (not .length) [...s]; // convert to array const unique = [...new Set(arr)]; // Map โ ordered, any key type, better than plain objects for algo work const mp = new Map(); mp.set('a', 1); mp.get('a'); // 1 mp.has('b'); // false mp.delete('a'); mp.size; mp.get('missing'); // undefined โ no KeyError // safe default: mp.get(key) ?? 0 // Plain objects โ fine for string/int keys in interviews const obj = { a: 1, b: 2 }; obj['c'] = 3; delete obj['a']; 'b' in obj; // true Object.keys(obj); // ['b', 'c'] Object.values(obj); // [2, 3] Object.entries(obj); // [['b',2], ['c',3]]
// for...of โ iterates VALUES (use for arrays, sets, maps) for (const x of arr) { ... } for (const x of s) { ... } // Set values for (const [k, v] of mp) { ... } // Map entries // for...in โ iterates KEYS (avoid for arrays โ iterates indices as strings) for (const key in obj) { ... } // use only for plain objects // Index + value (equiv of enumerate) for (let i = 0; i < arr.length; i++) { ... } arr.forEach((val, i) => { ... }); // Object iteration for (const [k, v] of Object.entries(obj)) { ... } // Map iteration for (const [k, v] of mp.entries()) { ... } mp.forEach((val, key) => { ... });
.filter().map() over a manual for-loop signals JS fluency. Waymo context: expect questions framed around vehicle/fleet data (filtering by status, grouping by type, transforming telemetry) where these are the natural fit.Explicit patterns โ memorise these
// โโ map(callback) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // callback receives: (currentItem, index, originalArray) // must RETURN the new value for each item // result: NEW array, same length as input const result = array.map((currentItem, index) => { return /* transformed version of currentItem */; }); // one-line arrow: implicit return (no curly braces needed) const ids = vehicles.map(vehicle => vehicle.id); // โโ filter(callback) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // callback receives: (currentItem, index, originalArray) // must RETURN true (keep) or false (discard) // result: NEW array, same length or shorter const result = array.filter((currentItem, index) => { return /* true to keep, false to drop */; }); // one-line arrow: implicit return const stopped = vehicles.filter(vehicle => vehicle.stationarySeconds > 300); // โโ reduce(callback, initialValue) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // callback receives: (accumulator, currentItem, index, originalArray) // must RETURN the accumulator โ this becomes the input for the next step // result: ONE value (number, string, object, Map, array โ anything) // // Two levels of arguments: // outer: reduce(callbackFn, initialValue) // โ the whole โ starting // function value of accumulator // inner: (accumulator, currentItem) โ parameters INSIDE the callback const result = array.reduce((accumulator, currentItem) => { // update accumulator using currentItem return accumulator; // ALWAYS return it, or next step gets undefined }, startingValue); // one-line: implicit return works when it fits const sum = arr.reduce((accumulator, item) => accumulator + item, 0); // reduce is great for building a Map โ Map is technically "one value" const byId = vehicles.reduce((accumulator, vehicle) => { accumulator.set(vehicle.id, vehicle); return accumulator; }, new Map()); // reduce for frequency count โ object is also "one value" const freq = arr.reduce((accumulator, item) => { accumulator[item] = (accumulator[item] ?? 0) + 1; return accumulator; }, {});
Full examples
// โโ map โโ transforms every element, returns a NEW array (same length) const doubled = arr.map(x => x * 2); const lengths = words.map(w => w.length); const ids = vehicles.map(v => v.id); // โโ filter โโ keeps elements where the test returns true, returns NEW array const evens = arr.filter(x => x % 2 === 0); const stopped = vehicles.filter(v => v.stationarySeconds > 300); // does NOT mutate the original array // โโ reduce โโ folds the array into a single value (number, object, Map, etc.) const sum = arr.reduce((accumulator, x) => accumulator + x, 0); const freq = arr.reduce((accumulator, x) => { accumulator[x] = (accumulator[x] ?? 0) + 1; return accumulator; }, {}); // build a Map with reduce โ Map is one object, so reduce is the right tool const byId = vehicles.reduce((accumulator, v) => { accumulator.set(v.id, v); return accumulator; }, new Map()); // โโ chaining โ the real power; each method returns a new array // "IDs of vehicles stopped > 5 min, sorted longest-stopped first" const result = vehicles .filter(v => v.stationarySeconds > 300) .sort((a, b) => b.stationarySeconds - a.stationarySeconds) .map(v => v.id); // โโ forEach โโ like map but returns undefined; use when you only want side effects arr.forEach((val, i) => console.log(i, val)); // don't chain off forEach โ it returns nothing // โโ find / findIndex โโ return the first match (or undefined / -1) arr.find(x => x > 3); // first value matching, or undefined arr.findIndex(x => x > 3); // first index matching, or -1 vehicles.find(v => v.id === 42); // lookup by field // โโ some / every โโ short-circuit boolean checks arr.some(x => x > 10); // true if ANY element matches (like Python any()) arr.every(x => x > 0); // true if ALL elements match (like Python all()) vehicles.some(v => v.faultCode !== null); // any vehicle has a fault? // โโ flat / flatMap โโ flatten nested arrays [[1,2],[3,4]].flat(); // [1,2,3,4] โ one level [[[1]],[[2]]].flat(2); // [1,2] โ two levels (or Infinity) arr.flatMap(x => [x, x * 2]); // map then flatten one level โ more efficient than .map().flat() // โโ Array.from โโ create array from iterables or with a fill function Array.from('hello'); // ['h','e','l','l','o'] Array.from(new Set([1,2,3])); // [1,2,3] Array.from({length: 5}, (_, i) => i); // [0,1,2,3,4] โ like range(5) Array.from({length: m}, () => new Array(n).fill(0)); // 2D grid init
// Two pointers let l = 0, r = arr.length - 1; while (l < r) { ... l++; r--; } // Sliding window const window = new Map(); let l = 0; for (let r = 0; r < s.length; r++) { window.set(s[r], (window.get(s[r]) ?? 0) + 1); while (isInvalid(window)) { window.set(s[l], window.get(s[l]) - 1); if (window.get(s[l]) === 0) window.delete(s[l]); l++; } } // BFS โ graph (array-as-queue; .shift() is O(n) but fine for interviews) function bfs(graph, start) { const queue = [start]; const visited = new Set([start]); while (queue.length) { const node = queue.shift(); for (const nei of (graph.get(node) ?? [])) { if (!visited.has(nei)) { visited.add(nei); queue.push(nei); } } } } // BFS โ 2D grid function bfsGrid(grid, startR, startC) { const rows = grid.length, cols = grid[0].length; const dirs = [[-1,0],[1,0],[0,-1],[0,1]]; const inBounds = (r, c) => r >= 0 && r < rows && c >= 0 && c < cols; const queue = [[startR, startC]]; const visited = new Set([`${startR},${startC}`]); while (queue.length) { const [r, c] = queue.shift(); for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; const key = `${nr},${nc}`; if (inBounds(nr, nc) && !visited.has(key)) { visited.add(key); queue.push([nr, nc]); } } } } // DFS โ iterative const stack = [start]; const visited = new Set([start]); while (stack.length) { const node = stack.pop(); for (const nei of graph.get(node) ?? []) { if (!visited.has(nei)) { visited.add(nei); stack.push(nei); } } } // DFS โ recursive (tree) function dfs(node) { if (!node) return 0; const left = dfs(node.left); const right = dfs(node.right); return 1 + Math.max(left, right); } // Binary search function binarySearch(arr, target) { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = Math.floor((lo + hi) / 2); if (arr[mid] === target) return mid; else if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; }
== or forget to pass a sort comparator.// 1. == vs === โ ALWAYS use === 0 == false // true โ type coercion 0 === false // false โ strict, no coercion '' == false // true null == undefined // true null === undefined // false // Rule: use === everywhere; the only exception is null checks: if (x == null) { ... } // catches both null and undefined โ intentional shortcut // 2. Reference equality โ objects and arrays compare by REFERENCE [1,2,3] === [1,2,3] // false โ different objects in memory {a:1} === {a:1} // false // Deep equality โ no built-in; options: JSON.stringify(a) === JSON.stringify(b) // works for simple objects, fragile for undefined/functions // In interviews: manually compare fields, or note you'd use _.isEqual(a, b) // 3. Sort without comparator is LEXICOGRAPHIC โ even for numbers [10, 9, 2].sort() // [10, 2, 9] โ "10" < "2" alphabetically [10, 9, 2].sort((a, b) => a - b) // [2, 9, 10] // 4. Falsy values โ all of these are falsy in JS false, 0, -0, 0n, "", '', ``, null, undefined, NaN // Note: empty array [] and empty object {} are TRUTHY if ([]) console.log('truthy') // prints โ unlike Python where [] is falsy // 5. NaN is not equal to itself NaN === NaN // false Number.isNaN(NaN) // true โ use this to check // 6. null vs undefined // null โ intentional absence (you set it) // undefined โ variable declared but not assigned, or missing property typeof null // "object" โ famous JS bug typeof undefined // "undefined" Array.isArray([]) // true โ use this, not typeof (which returns "object" for arrays) // 7. Type coercion with + "5" + 3 // "53" โ string concatenation wins "5" - 3 // 2 โ arithmetic (- forces numeric) +"5" // 5 โ unary + converts to number Number("5") // 5 parseInt("5px") // 5 โ stops at non-numeric // 8. var vs let vs const // var โ function-scoped, hoisted, can redeclare โ avoid // let โ block-scoped, can reassign // const โ block-scoped, cannot reassign binding (but object contents are mutable) const arr = [1, 2, 3]; arr.push(4); // ok โ mutating contents arr = [5]; // TypeError โ can't reassign the binding // 9. Array .shift() is O(n) โ use index pointer for performance-critical code // For interview BFS, .shift() is fine; just mention this tradeoff if asked let head = 0; while (head < queue.length) { const node = queue[head++]; // O(1) dequeue alternative } // 10. for...in on arrays โ iterates string indices, not values; avoid for (const i in [10, 20]) console.log(i); // "0", "1" โ strings! for (const x of [10, 20]) console.log(x); // 10, 20
Quick signal โ tool lookup:
| "top K" / frequency count | Map + sort, or max-heap (implement with sorted array) |
| BFS / shortest path | array queue with .shift() or index pointer |
| membership check | Set (O(1)) โ not array .includes() (O(n)) |
| key-value store | Map โ not plain object (avoids prototype key collisions) |
| graph adjacency / grouping | Map<node, node[]> |
| 2D grid coordinates | encode as `${r},${c}` string key in a Set |
| sort by custom rule | .sort((a, b) => ...) |
JavaScript Syntax Drills
const arr = [3, 1, 4, 1, 5]; arr.push(9); // [3,1,4,1,5,9] arr.pop(); // 9 (returns removed element) arr.slice(1, 3); // [1, 4] โ non-mutating const copy = [...arr]; // shallow copy const merged = [...a, ...b]; // concat without mutation // No Python arr[-1] โ use arr[arr.length - 1] or arr.at(-1) arr.at(-1); // last element (modern JS)
// Frequency count (Python Counter equivalent) const freq = new Map(); for (const c of s) { freq.set(c, (freq.get(c) ?? 0) + 1); } // Adjacency list (Python defaultdict(list) equivalent) const graph = new Map(); for (const [u, v] of edges) { if (!graph.has(u)) graph.set(u, []); if (!graph.has(v)) graph.set(v, []); graph.get(u).push(v); graph.get(v).push(u); }
// Dedup const unique = [...new Set(arr)]; // Visited set for BFS/DFS const visited = new Set(); visited.add(node); if (!visited.has(node)) { ... } // 2D grid visited โ encode coordinates as string const seen = new Set(); seen.add(`${r},${c}`); if (seen.has(`${r},${c}`)) { ... }
// Numbers ascending / descending nums.sort((a, b) => a - b); nums.sort((a, b) => b - a); // Sort objects by property intervals.sort((a, b) => a[0] - b[0]); // by start people.sort((a, b) => a.age - b.age); // Multi-key sort arr.sort((a, b) => a.len - b.len || a.name.localeCompare(b.name)); // Non-mutating sort const sorted = [...arr].sort((a, b) => a - b);
map transforms every element into something else (same length). filter removes elements that don't pass a test (shorter or equal length). reduce collapses the array into one value โ a number, object, Map, or anything else. Chain them: filter first (smaller array), then sort, then map.
// Waymo-style example: // "Return the IDs of vehicles stopped over 5 min, sorted longest-stopped first." const result = vehicles .filter(v => v.stationarySeconds > 300) // keep stopped vehicles .sort((a, b) => b.stationarySeconds - a.stationarySeconds) // longest first .map(v => v.id); // extract IDs only // Equivalent for-loop version โ valid, just less idiomatic in JS: const result2 = []; for (const v of vehicles) { if (v.stationarySeconds > 300) result2.push(v); } result2.sort((a, b) => b.stationarySeconds - a.stationarySeconds); const ids = result2.map(v => v.id); // reduce to build a frequency map const freq = arr.reduce((acc, x) => { acc[x] = (acc[x] ?? 0) + 1; return acc; }, {}); // some / every for early-exit boolean checks vehicles.some(v => v.faultCode !== null); // any fault? vehicles.every(v => v.batteryPct > 20); // all above threshold?
// Binary tree level order
function levelOrder(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length) {
const levelSize = queue.length;
const level = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level);
}
return result;
}// Max depth (recursive DFS) function maxDepth(root) { if (!root) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); } // DFS on 2D grid (recursive) function dfs(grid, r, c, visited) { const rows = grid.length, cols = grid[0].length; if (r < 0 || r >= rows || c < 0 || c >= cols) return; const key = `${r},${c}`; if (visited.has(key) || grid[r][c] === '0') return; visited.add(key); dfs(grid, r+1, c, visited); dfs(grid, r-1, c, visited); dfs(grid, r, c+1, visited); dfs(grid, r, c-1, visited); }
// Subsets / combinations (backtracking) function subsets(nums) { const result = []; function backtrack(start, current) { result.push([...current]); // spread to copy โ don't push reference! for (let i = start; i < nums.length; i++) { current.push(nums[i]); backtrack(i + 1, current); current.pop(); // undo choice } } backtrack(0, []); return result; }
Understand leading/trailing edges and how this is preserved. GFE has a Debounce 2 that adds .cancel() and .flush() โ do that after the basics.
// Debounce โ delay until user stops function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } // Throttle โ fire at most once per interval function throttle(fn, interval) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; }
- GFE: Debounce
- GFE: Debounce II (with .cancel / .flush)
- GFE: Throttle
Be confident with resolve/reject semantics. Promise.all rejects on first failure; Promise.any resolves on first success.
// Promise.all โ rejects if ANY rejects function promiseAll(promises) { return new Promise((resolve, reject) => { const results = []; let remaining = promises.length; if (remaining === 0) return resolve([]); promises.forEach((p, i) => { Promise.resolve(p).then(val => { results[i] = val; if (--remaining === 0) resolve(results); }).catch(reject); }); }); } // Promise.any โ resolves if ANY resolves function promiseAny(promises) { return new Promise((resolve, reject) => { const errors = []; let remaining = promises.length; if (remaining === 0) return reject(new AggregateError([], 'All promises were rejected')); promises.forEach((p, i) => { Promise.resolve(p).then(resolve).catch(err => { errors[i] = err; if (--remaining === 0) reject(new AggregateError(errors, 'All promises were rejected')); }); }); }); }
Both are recursion patterns. Curry uses fn.length to know when to stop collecting args. Flatten needs to handle arbitrary depth.
// Curry โ collect args until fn.length is satisfied function curry(fn) { return function curried(...args) { if (args.length >= fn.length) return fn(...args); return (...more) => curried(...args, ...more); }; } // Flatten โ recursive with depth function flatten(arr, depth = Infinity) { return arr.reduce((acc, val) => { if (Array.isArray(val) && depth > 0) { acc.push(...flatten(val, depth - 1)); } else { acc.push(val); } return acc; }, []); }
The hard part is handling circular references, Date, Map, and Set. JSON.parse(JSON.stringify(x)) is not a valid answer โ it drops functions, undefined, and Date objects.
function deepClone(val, seen = new Map()) {
if (val === null || typeof val !== 'object') return val;
if (seen.has(val)) return seen.get(val); // circular ref guard
if (val instanceof Date) return new Date(val);
if (val instanceof Set) {
const s = new Set();
seen.set(val, s);
val.forEach(v => s.add(deepClone(v, seen)));
return s;
}
if (val instanceof Map) {
const m = new Map();
seen.set(val, m);
val.forEach((v, k) => m.set(deepClone(k, seen), deepClone(v, seen)));
return m;
}
const clone = Array.isArray(val) ? [] : {};
seen.set(val, clone);
for (const key of Object.keys(val)) {
clone[key] = deepClone(val[key], seen);
}
return clone;
}OOP + method chaining. Implement on, off, emit. Common follow-up: once (fires exactly once, then auto-removes).
class EventEmitter {
constructor() { this._events = {}; }
on(event, listener) {
(this._events[event] ??= []).push(listener);
return this; // enables chaining
}
off(event, listener) {
if (!this._events[event]) return this;
this._events[event] = this._events[event].filter(l => l !== listener);
return this;
}
emit(event, ...args) {
(this._events[event] ?? []).forEach(l => l(...args));
return this;
}
once(event, listener) {
const wrapper = (...args) => { listener(...args); this.off(event, wrapper); };
return this.on(event, wrapper);
}
}Pick your weakest problem from Days 1โ6. Set a 35-minute timer. Solve it without looking at the solution. Then review carefully.
- Timed retry โ weakest problem from above (35 min, no peeking)
- Read: GFE Front-End Interview Playbook
// Select elements document.getElementById('my-id'); document.querySelector('.my-class'); // first match document.querySelectorAll('li'); // NodeList (use [...] to get array) // Create and insert const li = document.createElement('li'); li.textContent = 'Item'; li.classList.add('active'); ul.appendChild(li); ul.insertBefore(li, ul.firstChild); // prepend // Remove el.remove(); parent.removeChild(child); // Event listeners btn.addEventListener('click', (e) => { ... }); input.addEventListener('input', handler); // fires on every keystroke input.addEventListener('change', handler); // fires on blur/enter // Debounce โ delay execution until user pauses typing function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } const debouncedFilter = debounce(filterList, 300); input.addEventListener('input', debouncedFilter); // Render a list from data function renderList(items) { ul.innerHTML = ''; // clear existing for (const item of items) { const li = document.createElement('li'); li.textContent = item.name; ul.appendChild(li); } } // Toggle expand/collapse btn.addEventListener('click', () => { const isOpen = content.style.display !== 'none'; content.style.display = isOpen ? 'none' : 'block'; btn.textContent = isOpen ? 'โถ' : 'โผ'; });
- Build: filterable list with debounced search
- Build: expandable tree view (recursive render)
- Build: counter with stack-based undo history