TypeScript / Advanced / Lesson 18
Security, async boundaries, and a reporting capstone
Combine validated data, explicit permission checks, stable async snapshots, and selected output without treating static types as a security mechanism.
On this page
Lesson goals
- Separate validation, authorization, and safe output selection.
- Preserve state assumptions across asynchronous suspension.
- Integrate type and runtime tests into a reviewable capstone.
Types do not authenticate or authorize a request
A TypeScript interface for an authenticated user does not authenticate incoming bytes, and a validated record shape does not authorize access to it. Security decisions need runtime evidence from the real identity and permission system. Keep input validation, authorization, business transitions, and output encoding distinct. A union of approved operation names can help checked code, but an attacker-controlled string still requires boundary validation and permission checking. These lessons use only synthetic local data and perform no network or security-scanning operations. The examples illustrate contracts, not a complete production authentication design.
A readonly view is not a historical snapshot
An async function can suspend while shared objects change through other aliases. A readonly annotation restricts this access path but does not preserve earlier contents. The first example explicitly copies primitive numeric values before awaiting an already-resolved promise. The caller appends another number afterward, but the report uses the captured array copy. This is a shallow snapshot: nested objects would still require an appropriate ownership or copying policy. The example has no external I/O and no timing claim; promise scheduling only demonstrates that data lifetime and mutation assumptions can cross a suspension boundary.
export {};
async function totalLater(values: readonly number[]): Promise<number> {
const snapshot = [...values];
await Promise.resolve();
return snapshot.reduce((sum, value) => sum + value, 0);
}
const source = [1, 2];
const result = totalLater(source);
source.push(100);
result.then(value => console.log(value));
3
The snapshot contains one and two. The later caller mutation changes source but not the copied array used by the report.
Check permission and construct only public fields
The second synthetic function receives an actor already established by its caller, compares ownership, and constructs a selected public record. The actor identifier is not accepted as proof merely because a client supplied a matching number. A real system must derive that actor from its trusted authentication boundary. Returning a new public object ensures the internal token field is not serialized by accident. A type alias omitting that field would not remove it from the source object. Tests must verify denied access and actual output keys, not only whether the function signature type-checks.
export {};
type Actor = { id: number };
type Stored = { ownerId: number; title: string; internalToken: string };
function visible(actor: Actor, record: Stored): { title: string } | undefined {
if (actor.id !== record.ownerId) { return undefined; }
return { title: record.title };
}
const record: Stored = { ownerId: 1, title: "Draft", internalToken: "synthetic" };
console.log(JSON.stringify(visible({ id: 1 }, record)));
console.log(visible({ id: 2 }, record) === undefined);
{"title":"Draft"}
true
The matching actor receives only title. The other actor is denied; the internal field is never copied into the public result.
Make failures, cancellation, and resource bounds explicit
A Promise<T> models a fulfillment value and does not automatically specify the rejection type, cancellation, concurrency limit, or retry policy. Catch rejected values as unknown and validate structured errors when necessary. Copying input before await can stabilize data but does not roll back an external effect if a later step fails. Define commit points and cancellation behavior for real operations. Bound incoming list sizes and concurrent tasks to the application requirements rather than starting unbounded work from every accepted payload. Use an appropriate output encoding for the destination; a typed string is not automatically safe HTML, SQL, shell syntax, or a URL.
Deliver a small typed reporting system
Build a local package with unknown input decoding, domain functions, an explicit result union, a report service, and a selected presentation adapter. Keep module settings aligned with the host and test the emitted entry point. Cover malformed syntax, wrong shapes, numeric limits, absent fields, denied actors, empty lists, mutation across await, and actual output keys. Add compile-only cases for invalid discriminants and generic key relationships. Measure checker and runtime costs separately after correctness passes. Document compiler and runtime versions, supported input limits, failure channels, and the assumptions of untyped dependencies. A complete contract is understandable without trusting assertions or reading every implementation line.
Common errors and fixes
A typed actor identifier is trusted from client input.
- Why it happens
- Static shape was confused with authenticated identity.
- How to fix it
- Derive identity and permission from the real trusted boundary.
- Prevent it next time
- Test denied actors as well as valid shapes.
An async readonly input changes before use.
- Why it happens
- The annotation did not capture a stable snapshot.
- How to fix it
- Copy or encapsulate the data according to its required lifetime.
- Prevent it next time
- Review aliases across every suspension point.
A typed string is inserted into an unsafe output context.
- Why it happens
- String typing was mistaken for context-sensitive encoding.
- How to fix it
- Use the destinationโs supported safe encoding or parameterization.
- Prevent it next time
- Keep validation and output handling as separate requirements.
Check your understanding
1. Remove the snapshot copy and read values directly after await. What result would the synthetic example produce, and what contract changed?
Show worked solution
It would include the caller-appended one hundred and total one hundred three. The function would observe the later shared state rather than the state at call time. Both behaviors can be deliberate, but readonly does not choose between them. Preserve a regression test for the chosen temporal contract, especially when nested mutable records replace primitive numbers.
2. A developer replaces the explicit public object with a Stored value assigned to a title-only interface. Explain the exposure risk and a useful test.
Show worked solution
The same runtime object still contains internalToken, so serializing it can expose the field despite the narrow static view. Retain explicit field selection and assert that serialized keys contain only title. Also test an unauthorized actor receives no record. Type assignability proves neither data removal nor permission to disclose it.
Key takeaways
- Types support security reasoning but do not enforce identity or encoding.
- Stabilize data deliberately across async boundaries.
- Test denied access and real serialized output.