TypeScript · Values, Promises and payment-safe code
The 60-second cheat sheet#
TypeScript checks a model of JavaScript before execution; runtime JSON still needs validation. An async function returns a Promise. Await the HTTP response, read its synchronous status, then await JSON. Run independent reads concurrently; sequence operations that depend on an ID or state change.
Syntax you should write without hesitation#
| Syntax | Meaning / caution |
|---|---|
| const / let | Fixed binding / reassignable binding; const does not freeze an object |
| === | Strict equality; different object references are not deeply equal |
| null / undefined | Explicit absence / absent or unassigned; respect API distinction |
| ?. / ?? | Safe property access / default only for nullish values, preserving 0 and false |
| Spread / destructuring | Convenient shallow copy/unpacking; nested references remain shared |
| map/filter/find | Transform/select/first match or undefined |
| some/every/reduce | Existence/all/accumulation; empty every is true, reduce needs an initial value |
| Set / Map | Unique primitive values / keyed values; object identity matters |
| type / interface | Aliases can express unions; both describe shapes; neither validates bytes |
| unknown / any | Prove shape before use / opt out of checking |
export type Row = { id: string; amountMinor: number; status: string };
export const successful = (rows: Row[]) => rows.filter(r => r.status === 'CAPTURED');
export const total = (rows: Row[]) => rows.reduce((sum, r) => sum + r.amountMinor, 0);
export const uniqueIds = (rows: Row[]) => [...new Set(rows.map(r => r.id))];
export const numericOrder = (values: number[]) => [...values].sort((a, b) => a - b);
The total example assumes compatible currency and safe bounded amounts; do not sum mixed currencies or exceed safe-integer range. sort() mutates and defaults to lexical order. All four helpers need empty-input tests; find/access operations need missing-value handling under strict indexed access.
Promise versus response versus body#
// Scoped Playwright test body with request and imported expect.
const responsePromise = request.get('/sandbox/payments/owned-id');
// Wrong: responsePromise.status(); // Promise<APIResponse>, not APIResponse
const response = await responsePromise;
expect(response.status()).toBe(200);
const data: unknown = await response.json();
expect(data).toEqual(expect.objectContaining({ id: 'owned-id' }));
Promise states: pending, fulfilled and rejected. Await yields the fulfillment value or throws the rejection. HTTP 4xx/5xx can still resolve to APIResponse unless failure behavior is configured; assert status explicitly. A transport failure is different from a returned business error. Promise<void> means no useful fulfillment value, not no Promise.
Dependency order and concurrency#
// Scoped test body: independent reads, then a dependent create/read sequence.
const [routes, methods] = await Promise.all([
request.get('/sandbox/routing-rules'), request.get('/sandbox/payment-methods'),
]);
expect(routes.status()).toBe(200); expect(methods.status()).toBe(200);
const created = await request.post('/test-support/payments', {
data: { amountMinor: 1000, currency: 'GEL' },
});
expect(created.status()).toBe(201);
const payment: { id: string } = await created.json(); // validate runtime shape in reusable code
const fetched = await request.get(`/sandbox/payments/${encodeURIComponent(payment.id)}`);
expect(fetched.status()).toBe(200);
Promise.all preserves input result order, rejects when an input rejects and does not cancel the other operations. It overlaps asynchronous waiting, not necessarily CPU threads. allSettled reports each outcome and still needs explicit failure handling. Bound concurrency to service capacity and avoid competing mutable records unless concurrency is the test.
export async function processInOrder<T>(items: T[], run: (item: T) => Promise<void>): Promise<void> {
for (const item of items) await run(item);
}
export async function processIndependent<T>(items: T[], run: (item: T) => Promise<void>): Promise<void> {
await Promise.all(items.map(run)); // bounded input, independent operations only
}
// Wrong: await items.forEach(async item => await run(item));
// forEach returns undefined and ignores callback Promises.
Runtime narrowing and meaningful states#
export type PaymentState =
| { status: 'PENDING'; id: string }
| { status: 'CAPTURED'; id: string; providerReference: string }
| { status: 'DECLINED'; id: string; reason: string };
export function explain(state: PaymentState): string {
if (state.status === 'DECLINED') return state.reason;
if (state.status === 'CAPTURED') return state.providerReference;
return 'Await documented completion';
}
A discriminated union makes incompatible fields harder to misuse. A generic helper preserves type relationships; it is not runtime schema validation. Use import type, exported modules and strict compiler checks. Node process.env values are strings or undefined: validate before login/configuration. Playwright transpilation does not replace tsc --noEmit.
Money parsing exercise#
// Standalone exercise for a declared TWO-decimal currency only.
// Not a universal currency parser; does not accept grouping or scientific notation.
export function parseTwoDecimalAmount(input: string): number {
const match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(input);
if (!match) throw new Error('Use a nonnegative decimal with at most two decimals');
const whole = Number(match[1]);
const fraction = Number((match[2] ?? '').padEnd(2, '0'));
const result = whole * 100 + fraction;
if (!Number.isSafeInteger(result)) throw new Error('Amount is too large');
return result;
}
Ideal examples: 0 → 0, 12.3 → 1230, 12.34 → 1234; reject -1, 1.234, 1e3, whitespace and empty input under this teaching contract. Decide separately whether zero is allowed for payment creation. Leading-zero policy and supported currencies must come from real requirements.
Errors and event-loop reasoning#
Synchronous code runs to completion; Promise continuations run after the current stack as microtasks; timers schedule later tasks, not exact deadlines. Await suspends the current async function, allowing other work. A busy CPU can delay everything in that process. Use observed completion rather than assuming setTimeout proves business success.
export async function ownedResource<T>(
run: () => Promise<T>, cleanup: () => Promise<void>,
report: (error: unknown) => void,
): Promise<T> {
let failed = false;
try { return await run(); }
catch (error) { failed = true; throw error; }
finally {
try { await cleanup(); }
catch (error) { report(error); if (!failed) throw error; }
}
}
The report callback should not throw and must redact sensitive values; otherwise it can mask the original error. Finally attempts teardown but cannot guarantee it during process termination or service outage. Retrying an unclassified payment write is not a generic exception-recovery strategy.
Interview answer and references#
30 seconds: “I separate static types from runtime validation and Promise from resolved value. I await the response, assert status and await its body. I sequence dependent payment operations, overlap only independent reads and preserve failures through cleanup. I use integer/defined decimal money representation rather than unchecked floating-point arithmetic.”
90-second answer: review an async API helper
I inspect which calls produce Promises, which values are needed later and what happens on rejection or HTTP error. A missing await may make a test finish too early; async forEach also does not join its callbacks. I use Promise.all only for independent bounded operations and remember siblings keep running after rejection. The response needs exact status and runtime shape checks. I keep a finally/fixture cleanup boundary around owned data, preserving secondary failure evidence. For a payment timeout I check commitment and idempotency semantics rather than retry blindly. Finally I typecheck the module and test malformed input, missing IDs, negative results and cleanup failures, not just the successful example.
Memory hook: await the thing you need; types are not bytes. References: TypeScript narrowing, Promise.all. Next: Playwright.
Quick practice#
What does async return?
- Always a Promise, including Promise
- A thrown exception becomes rejection and await obtains fulfillment or throws.
Pitfall: Expecting a synchronous bare value from async.
Why does request.get(...).status fail without await?
- get returns Promise, so await the response before its synchronous status method
- json returns another Promise and needs a separate await.
Pitfall: Casting the Promise to any to silence the error.
Does HTTP 500 necessarily reject the request Promise?
- Not with ordinary APIResponse behavior unless failure settings require it
- Assert the status as well as handling transport rejection.
Pitfall: Catch-only tests silently accept returned server errors.