Theme 01 · Type-Safety Maximalist, But Pragmatic
Parsed inputs
Explanation
Parsed inputs
Plain Human Explanation
Data from forms, URLs, webhooks, files, and queues is not trustworthy just because TypeScript has a type for it. Parsed inputs mean the app checks outside data once, near the edge, and then passes a cleaner trusted shape inward.
Technical Explanation
Treat outside data as unknown until a parser proves the required fields, value ranges, and allowed variants. The parser can return a typed value, null, or a typed error. After that point, core code should accept the parsed type instead of repeating typeof, optional chaining, and casts everywhere.
Why It Matters
- User impact: invalid requests fail predictably instead of causing partial writes or confusing errors.
- Product behavior: required fields and value limits are enforced before business rules run.
- Risk:
as SomeTypecan hide bad runtime data from the compiler and from reviewers. - Decision point: use this for every boundary where data enters from outside trusted code.
The Core Move
Parse at the boundary, then pass trusted values into the rest of the system. Do not let raw request bodies drift into product logic.
Small Example
Parsed inputs: Small Example
Bad TypeScript Example
type SignupInput = {
email: string;
plan: "free" | "pro";
};
export function startSignup(body: unknown) {
const input = body as SignupInput;
return `Starting ${input.plan} signup for ${input.email.toLowerCase()}`;
}
type SignupInput = {
email: string;
plan: "free" | "pro";
};
export function startSignup(body: unknown) {
const input = body as SignupInput;
return `Starting ${input.plan} signup for ${input.email.toLowerCase()}`;
}
Good TypeScript Example
type SignupInput = {
email: string;
plan: "free" | "pro";
};
function parseSignupInput(body: unknown): SignupInput | null {
if (typeof body !== "object" || body === null) return null;
const value = body as Record<string, unknown>;
if (typeof value.email !== "string" || !value.email.includes("@")) return null;
if (value.plan !== "free" && value.plan !== "pro") return null;
return { email: value.email.toLowerCase(), plan: value.plan };
}
export function startSignup(body: unknown) {
const input = parseSignupInput(body);
if (!input) return { ok: false, error: "invalid-signup-input" };
return { ok: true, message: `Starting ${input.plan} signup for ${input.email}` };
}
type SignupInput = {
email: string;
plan: "free" | "pro";
};
function parseSignupInput(
body: unknown,
): SignupInput | null {
if (
typeof body !== "object" ||
body === null
)
return null;
const value = body as Record<
string,
unknown
>;
if (
typeof value.email !== "string" ||
!value.email.includes("@")
)
return null;
if (
value.plan !== "free" &&
value.plan !== "pro"
)
return null;
return {
email: value.email.toLowerCase(),
plan: value.plan,
};
}
export function startSignup(body: unknown) {
const input = parseSignupInput(body);
if (!input)
return {
ok: false,
error: "invalid-signup-input",
};
return {
ok: true,
message: `Starting ${input.plan} signup for ${input.email}`,
};
}
What Changed
- The bad version tells TypeScript to trust data it has not checked.
- The good version proves the email and plan before product code uses them.
- Later code receives a real
SignupInput, not a hopeful cast.
Realistic Example
Parsed inputs: Realistic Example
This example uses a route handler where query strings and JSON bodies arrive as loose data.
Bad TypeScript Example
type SearchRequest = {
term: string;
page: number;
includeArchived: boolean;
};
type UserStore = {
search(
term: string,
page: number,
includeArchived: boolean,
): Promise<
Array<{
id: string;
email: string;
}>
>;
};
export async function searchUsers(
req: {
query: Record<string, string | undefined>;
},
store: UserStore,
) {
const input = req.query as unknown as SearchRequest;
return store.search(input.term, input.page, input.includeArchived);
}
type SearchRequest = {
term: string;
page: number;
includeArchived: boolean;
};
type UserStore = {
search(
term: string,
page: number,
includeArchived: boolean,
): Promise<
Array<{
id: string;
email: string;
}>
>;
};
export async function searchUsers(
req: {
query: Record<
string,
string | undefined
>;
},
store: UserStore,
) {
const input =
req.query as unknown as SearchRequest;
return store.search(
input.term,
input.page,
input.includeArchived,
);
}
Good TypeScript Example
type SearchRequest = {
term: string;
page: number;
includeArchived: boolean;
};
type UserStore = {
search(
term: string,
page: number,
includeArchived: boolean,
): Promise<
Array<{
id: string;
email: string;
}>
>;
};
function parseSearchRequest(query: Record<string, string | undefined>): SearchRequest | null {
const term = query.term?.trim();
const page = Number(query.page ?? "1");
const includeArchived = query.includeArchived === "true";
if (!term) return null;
if (!Number.isInteger(page) || page < 1 || page > 100) return null;
return { term, page, includeArchived };
}
export async function searchUsers(
req: {
query: Record<string, string | undefined>;
},
store: UserStore,
) {
const input = parseSearchRequest(req.query);
if (!input) return { status: 400, body: { error: "invalid-search-query" } };
const users = await store.search(input.term, input.page, input.includeArchived);
return { status: 200, body: { users } };
}
type SearchRequest = {
term: string;
page: number;
includeArchived: boolean;
};
type UserStore = {
search(
term: string,
page: number,
includeArchived: boolean,
): Promise<
Array<{
id: string;
email: string;
}>
>;
};
function parseSearchRequest(
query: Record<string, string | undefined>,
): SearchRequest | null {
const term = query.term?.trim();
const page = Number(query.page ?? "1");
const includeArchived =
query.includeArchived === "true";
if (!term) return null;
if (
!Number.isInteger(page) ||
page < 1 ||
page > 100
)
return null;
return {
term,
page,
includeArchived,
};
}
export async function searchUsers(
req: {
query: Record<
string,
string | undefined
>;
},
store: UserStore,
) {
const input = parseSearchRequest(
req.query,
);
if (!input)
return {
status: 400,
body: {
error: "invalid-search-query",
},
};
const users = await store.search(
input.term,
input.page,
input.includeArchived,
);
return {
status: 200,
body: {
users,
},
};
}
What Changed
- The bad version treats URL strings as if they already had boolean and number types.
- The good version converts and validates once at the route boundary.
- The store receives a safe, bounded search request instead of raw query values.
System Example
Parsed inputs: System Example
At system scale, parsing protects every downstream worker, logger, and database write from one bad external payload.
Larger System-Level Bad TypeScript Example
type BillingPorts = {
log: {
info(event: string, payload: unknown): Promise<void>;
};
subscriptions: {
markPaid(customerId: string, amountPaidCents: number): Promise<void>;
};
};
export async function handleWebhook(payload: any, ports: BillingPorts) {
await ports.log.info("billing.webhook", payload);
if (payload.type === "invoice.paid") {
await ports.subscriptions.markPaid(payload.data.customer, payload.data.amount_paid);
}
return { ok: true };
}
type BillingPorts = {
log: {
info(
event: string,
payload: unknown,
): Promise<void>;
};
subscriptions: {
markPaid(
customerId: string,
amountPaidCents: number,
): Promise<void>;
};
};
export async function handleWebhook(
payload: any,
ports: BillingPorts,
) {
await ports.log.info(
"billing.webhook",
payload,
);
if (payload.type === "invoice.paid") {
await ports.subscriptions.markPaid(
payload.data.customer,
payload.data.amount_paid,
);
}
return {
ok: true,
};
}
Larger System-Level Good TypeScript Example
type InvoicePaidWebhook = {
type: "invoice.paid";
customerId: string;
amountPaidCents: number;
};
type BillingPorts = {
log: {
info(event: string, payload: Record<string, string>): Promise<void>;
};
subscriptions: {
markPaid(customerId: string, amountPaidCents: number): Promise<void>;
};
};
function parseInvoicePaidWebhook(payload: unknown): InvoicePaidWebhook | null {
if (typeof payload !== "object" || payload === null) return null;
const event = payload as {
type?: unknown;
data?: {
customer?: unknown;
amount_paid?: unknown;
};
};
if (event.type !== "invoice.paid") return null;
if (typeof event.data?.customer !== "string") return null;
if (!Number.isInteger(event.data.amount_paid) || event.data.amount_paid < 0) return null;
return { type: "invoice.paid", customerId: event.data.customer, amountPaidCents: event.data.amount_paid };
}
export async function handleWebhook(payload: unknown, ports: BillingPorts) {
const event = parseInvoicePaidWebhook(payload);
if (!event) return { ok: false, error: "ignored-webhook" };
await ports.log.info("billing.invoice_paid", { customerId: event.customerId });
await ports.subscriptions.markPaid(event.customerId, event.amountPaidCents);
return { ok: true };
}
type InvoicePaidWebhook = {
type: "invoice.paid";
customerId: string;
amountPaidCents: number;
};
type BillingPorts = {
log: {
info(
event: string,
payload: Record<string, string>,
): Promise<void>;
};
subscriptions: {
markPaid(
customerId: string,
amountPaidCents: number,
): Promise<void>;
};
};
function parseInvoicePaidWebhook(
payload: unknown,
): InvoicePaidWebhook | null {
if (
typeof payload !== "object" ||
payload === null
)
return null;
const event = payload as {
type?: unknown;
data?: {
customer?: unknown;
amount_paid?: unknown;
};
};
if (event.type !== "invoice.paid")
return null;
if (
typeof event.data?.customer !== "string"
)
return null;
if (
!Number.isInteger(
event.data.amount_paid,
) ||
event.data.amount_paid < 0
)
return null;
return {
type: "invoice.paid",
customerId: event.data.customer,
amountPaidCents: event.data.amount_paid,
};
}
export async function handleWebhook(
payload: unknown,
ports: BillingPorts,
) {
const event =
parseInvoicePaidWebhook(payload);
if (!event)
return {
ok: false,
error: "ignored-webhook",
};
await ports.log.info(
"billing.invoice_paid",
{
customerId: event.customerId,
},
);
await ports.subscriptions.markPaid(
event.customerId,
event.amountPaidCents,
);
return {
ok: true,
};
}
What Changed
- The bad version spreads raw provider data through logging and subscription updates.
- The good version turns one known provider event into one trusted internal command.
- Downstream code no longer needs to know the provider’s nested payload shape.
When To Use It
Parsed inputs: When To Use It
Use This When
- Data comes from a request body, query string, webhook, queue message, file, environment variable, or local storage.
- Core code expects numbers, booleans, dates, enums, or required fields.
- Bad data could create partial writes, confusing errors, or unsafe logs.
Avoid This When
- The value was already created by a trusted constructor in the same module.
- The field is harmless display text and no business rule depends on it.
- You are using a schema library that already performs the parse at this exact boundary.
Tradeoffs
Parsing adds boundary code, but it removes repeated checks from the rest of the system. Keep parsers close to the incoming format and return small internal types.
Related Concepts
- Branded types
- Typed errors
- Strict TypeScript settings
Practice Prompt
Parsed inputs: Practice Prompt
Beginner Exercise
Find one as SomeType cast near a request, webhook, or queue boundary. List the fields the code assumes are present.
Intermediate Exercise
Replace that cast with a parser that accepts unknown or raw query strings and returns either a trusted input or null.
Stretch Exercise
Change one downstream function so it accepts only the parsed type. Remove any duplicate checks that are now handled by the parser.
Reflection Question
Where does outside data first become trusted, and is that location close enough to the boundary?
Suggest an edit
Leave a private editorial note. This creates a GitHub issue for this curriculum page.