P0 · Chapter 01 / 15
TypeScript / JavaScript
The 60-second cheat sheet#
JavaScript executes application logic; TypeScript adds compile-time constraints and emits JavaScript. Types do not validate network JSON at runtime. An async function always returns a Promise; await obtains its fulfillment value or throws its rejection. Use concurrency only when operations do not depend on each other's results or shared mutable state.
Interviewers test whether asynchronous code actually observes the intended response and whether failures propagate to the runner. A missing await can make a test pass before its important work completes.
Values, functions and syntax#
| Construct | Decision / gotcha |
|---|---|
const / let |
Prefer const for bindings not reassigned; const does not freeze an object |
| Primitives / references | Strings/numbers/booleans copy values; object assignment shares a reference |
null / undefined |
Explicit absence vs missing/unassigned; model API distinctions deliberately |
=== |
Strict equality avoids coercion; separate objects are not deeply equal |
| Falsy values | false, 0, -0, empty string, null, undefined, NaN; empty arrays/objects are truthy |
?. / ?? |
Safe access / default only for nullish; || also replaces valid 0/false/empty string |
| Arrow function | Lexical this; avoid using one as an object method needing dynamic this |
| Closure | Function retains access to its lexical environment; beware shared mutable counters in tests |
| Spread / rest | Shallow copy / collect remaining arguments; nested objects remain shared |
// Standalone TypeScript: value and collection patterns.
type Profile = { id: string; attempts?: number; tags: string[] };
const original: Profile = { id: 'qa-1', attempts: 0, tags: ['test'] };
const copy = { ...original, tags: [...original.tags, 'verified'] };
const attempts = original.attempts ?? 3; // 0 remains 0
const { id, ...metadata } = copy;
const makePrefix = (prefix: string) => (value: string) => `${prefix}:${value}`;
const correlate = makePrefix('verification');
console.log(id, metadata, attempts, correlate('qa-1'));
Promise versus response versus body#
Promise states are pending, fulfilled and rejected. Settled means fulfilled or rejected. A resolved Promise may adopt another Promise's eventual outcome; do not confuse “resolved” with synchronous availability.
// Scoped inside a Playwright test with { request } and imported expect.
const responsePromise = request.get('/api/users/qa-1');
// WRONG: responsePromise.status(); // Promise<APIResponse> has no status()
const response = await responsePromise;
expect(response.status()).toBe(200); // synchronous status read
const body: unknown = await response.json(); // asynchronous body parsing
expect(body).toEqual(expect.objectContaining({ id: 'qa-1' }));
Request methods usually resolve for HTTP error responses too. A 500 is not automatically a rejected Promise unless failure behavior is configured. Transport failure and HTTP failure need different evidence.
Promise.all and sequential await#
| Pattern | Choose when | Failure behavior |
|---|---|---|
await a(); await b() |
b needs a's ID or side effect | b never starts if a throws |
Promise.all([a(), b()]) |
Independent operations, both needed | Rejects when one rejects; other operation continues |
Promise.allSettled(...) |
Need every success/failure report | Returns per-operation outcome; inspect all failures |
| Bounded worker queue | Many operations would overload service | Must manage concurrency and aggregate errors |
// Scoped Playwright test body; illustrative API contracts.
const [users, profiles] = await Promise.all([
request.get('/api/users'), request.get('/api/profiles'),
]);
expect(users.status()).toBe(200);
expect(profiles.status()).toBe(200);
// Dependent POST -> GET: cannot schedule GET before obtaining id.
const created = await request.post('/api/users', { data: { name: 'QA' } });
expect(created.status()).toBe(201);
const user: { id: string } = await created.json(); // validate shape in production
const fetched = await request.get(`/api/users/${encodeURIComponent(user.id)}`);
expect(fetched.status()).toBe(200);
Concurrency overlaps waiting for I/O; it does not imply multiple JavaScript CPU threads. Too much concurrency can trigger rate limits, pool starvation or data collisions. If one request fails, Promise.all does not roll back writes already committed elsewhere.
Iteration and async traps#
// Standalone reusable helpers. forEach ignores callback Promises.
async function sequential<T>(items: T[], run: (item: T) => Promise<void>): Promise<void> {
for (const item of items) await run(item);
}
async function independent<T>(items: T[], run: (item: T) => Promise<void>): Promise<void> {
await Promise.all(items.map(run)); // only for bounded independent inputs
}
// WRONG: await items.forEach(async item => await run(item));
// forEach returns undefined, so awaiting it doesn't await callbacks.
map transforms, filter selects, find returns first or undefined, some tests existence, every tests all (true on an empty array), reduce accumulates. Supply a reduce initial value. sort() mutates and defaults to lexical ordering; use [...values].sort((a,b) => a-b). Set deduplicates primitive IDs; Map indexes by key. Object keys compared by identity do not deduplicate equivalent DTOs.
Error propagation and event loop#
Use throw new Error(message) when a helper cannot satisfy its contract. Catch to enrich diagnostics, recover deliberately or clean up; silently swallowing an error creates false passes. finally runs after a returned awaited operation settles, but the process can still be killed. A thrown cleanup error can replace the original failure.
// Standalone diagnostic pattern; no swallowed operation failures.
async function withCleanup<T>(run: () => Promise<T>, clean: () => Promise<void>): Promise<T> {
let failed = false;
try { return await run(); }
catch (error) { failed = true; throw error; }
finally {
try { await clean(); }
catch (error) {
if (!failed) throw error;
console.error('Cleanup also failed', error); // redact sensitive values
}
}
}
Synchronous JavaScript runs to completion. Promise continuations run as microtasks after the current stack; timers enqueue later tasks, not exact deadlines. setTimeout(..., 0) does not run immediately and a busy CPU delays it. await yields the current async function, not the entire process. In tests, arbitrary timer sleeps replace evidence with a timing assumption.
TypeScript contracts and narrowing#
| Feature | Senior decision |
|---|---|
interface / type |
Both describe object shape; interfaces can merge/extend; type aliases also express unions/tuples |
| Optional property | May be absent; distinguish from explicitly null contract |
unknown / any |
Unknown requires proof before use; any disables checking |
| Union / discriminant | Express mutually exclusive states and narrow by status |
Generic <T> |
Preserve relationships between inputs/outputs; not runtime validation |
as User |
Compiler assertion only; cannot repair malformed JSON |
Record<string, number> |
String-keyed counts; Map avoids prototype/key pitfalls |
// types/verification.ts: exported DTO + actual runtime guard.
export type Verification =
| { status: 'PENDING'; id: string }
| { status: 'VERIFIED'; id: string; verifiedAt: string }
| { status: 'REJECTED'; id: string; reason: string };
export function parseUser(value: unknown): { id: string } {
if (typeof value !== 'object' || value === null ||
!('id' in value) || typeof value.id !== 'string' || !value.id) {
throw new Error('Invalid user payload: expected nonempty string id');
}
return { id: value.id };
}
export function first<T>(values: readonly T[]): T | undefined { return values[0]; }
Use strict compiler options including strict: true and consider noUncheckedIndexedAccess. Export/import types with ES modules (import type). Node provides process.env and filesystem/path APIs, unlike browser globals. Environment variables are strings or undefined: validate before using. TypeScript transpilation is not a typecheck in every runner; run tsc --noEmit explicitly. Resolve paths from a deliberate project root or import.meta.url, not a guessed working directory.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"types": ["node"]
},
"include": ["tests/**/*.ts", "pages/**/*.ts", "fixtures/**/*.ts", "clients/**/*.ts"]
}
This scoped Node/Playwright tsconfig assumes TypeScript, Node types and project dependencies installed. Module behavior also depends on package.json type and file extensions. Stricter indexed access means array/map lookups need a missing-value path rather than an assertion that they must exist.
Six coding exercises with ideal answers#
// Standalone TypeScript exercises 1-4. O(n) expected time, O(k) extra space.
type Result = { id: string; status: number };
export const dedupe = (ids: string[]) => [...new Set(ids)];
export const countErrors = (rows: Result[]) => rows.filter(r => r.status >= 400).length;
export const successes = (rows: Result[]) => rows.filter(r => r.status >= 200 && r.status < 300);
export function groupByStatus(rows: Result[]): Map<number, Result[]> {
const groups = new Map<number, Result[]>();
for (const row of rows) {
const bucket = groups.get(row.status) ?? [];
bucket.push(row); groups.set(row.status, bucket);
}
return groups;
}
// Exercise 5: retry only read-only/contract-idempotent work. No payment replay.
export async function retryRead<T>(read: () => Promise<T>, attempts = 3): Promise<T> {
if (!Number.isInteger(attempts) || attempts < 1) throw new Error('Invalid attempts');
for (let i = 1; ; i++) {
try { return await read(); }
catch (error) { if (i >= attempts) throw error; }
}
}
// Exercise 6: parse unknown JSON with parseUser above, never `as` alone.
Discuss empty input, case-sensitive IDs, nulls disallowed by type but possible at runtime, and invalid HTTP status values. Error-count definition is contract-specific (400+ includes client and server errors). The retry exercise deliberately has no backoff; a real service client needs bounded elapsed time, retry classification, jitter and cancellation support. Parsing costs O(payload fields examined).
Wrong versus right decisions#
| Wrong | Right | Reason |
|---|---|---|
expect(request.get(...)).toBeTruthy() |
Await and assert status/body | A Promise object is truthy even for a future error |
| Concurrent POST and dependent GET | Read created ID then GET | Required data dependency |
| Async forEach | for-of or Promise.all(map) | Wait for actual work |
body as User proves schema |
Runtime guard or optional schema library | Types erased at runtime |
| Catch then console.log and return | Rethrow or assert meaningful recovery | Test must fail for broken behavior |
Interview response#
30 seconds: “Async functions return Promises. I await the response before reading its status and await JSON separately. I sequence dependent calls, such as create then fetch by returned ID. For independent reads I can use Promise.all, which overlaps I/O but neither cancels other requests nor validates HTTP status.”
90-second senior answer: concurrency and error design
I first draw the dependency graph. Creating a user must finish before its ID can be used, whereas two unrelated catalog reads can start together. Promise.all preserves input order in its result array, rejects on a rejected input, and leaves remaining operations running. HTTP 500 may still produce a resolved APIResponse, so I assert the status explicitly and then validate the body at runtime. I limit concurrency to the service's capacity and avoid sharing mutable accounts. I use allSettled when I need a complete diagnostic report, then explicitly fail if required operations failed. Cleanup belongs in a finally or fixture teardown, with secondary errors preserved. For payments I never treat retry as harmless: I inspect the backend's idempotency and reconciliation contract first.
Memory hook: Promise → await response → inspect status → await body → validate meaning.
Quick practice · reveal answers#
What does async return?
- Always a Promise, including Promise<void>.
- A thrown error becomes rejection; await obtains fulfillment or throws.
Pitfall: Assuming an async function returns its bare value synchronously.
Why can response.status fail after request.get?
- request.get returns Promise<APIResponse>.
- Await it first; status() is synchronous and json() must be awaited.
Pitfall: Confusing response, Promise and parsed body.
When should you use Promise.all?
- Independent operations that can safely overlap.
- Results follow input order; assert each HTTP response.
Pitfall: Concurrent dependent POST and GET.
Does Promise.all cancel other requests?
- No.
- It rejects on a rejected input but remaining operations continue; partial side effects may exist.
Pitfall: Treating rejection as transaction rollback.
Official references and related topics#
Optional online further reading; all core study material is included above.