CodeRigor

LESSON

Model objects, arrays and optional properties

Keep readonly inputs, fresh output objects and the distinction between an absent label and an empty string.

An interface describes object structure for the compiler. It does not allocate an object or validate an external record. A readonly array prevents mutation through that typed reference; it is not a runtime freeze or a guarantee that every nested object is immutable.

Our display helper accepts records with an optional label and returns fresh display rows. An absent label falls back to service, but an empty label is intentionally preserved. That small distinction exposes the difference between absence and truthiness before a larger migration depends on it.

Model objects, arrays and optional properties
start / 3
Keep readonly inputs, fresh output objects and the distinction between an absent label and an empty string.
Model objects, arrays and optional properties
Keep readonly inputs, fresh output objects and the distinction between an absent label and an empty string.

Use the nullish operator ?? for absence rather than || for truthiness. Empty strings, zero and false are different values from missing data; choosing the wrong fallback quietly changes the API. Here an empty label is valid.

With exactOptionalPropertyTypes, label?:string permits omission but does not permit explicitly assigning undefined in a typed object unless undefined is included in that property’s type. Reading the property can still produce undefined, so the helper handles that branch deliberately.

TypeScript
export interface DisplayJob { service: string; label?: string; }
export interface DisplayRow { service: string; label: string; }
export function displayRows(jobs: readonly DisplayJob[]): DisplayRow[] {
    return jobs.map(job => ({ service: job.service, label: job.label ?? job.service }));
}
console.log(JSON.stringify(displayRows([{service:"api"},{service:"db",label:""}])));

Fresh output objects avoid aliasing the original record. The tests freeze inputs and inspect identity as well as values. That checks behavior beyond whether a type declaration compiles.

The compiler check contrasts an omitted optional property with an explicitly undefined value under the chosen settings. These settings are part of this lab’s contract; another project’s tsconfig may choose different optional-property behavior.

Keep the external-data boundary
start / 3
Type relationships are checked before execution. JSON fields still need actual runtime checks.
Keep the external-data boundary
Type relationships are checked before execution. JSON fields still need actual runtime checks.
TypeScript
interface Options { label?: string; }
const omitted: Options = {};
const empty: Options = {label:""};
console.log(omitted.label ?? "fallback");
console.log(JSON.stringify(empty.label ?? "fallback"));

Implement fresh display rows and verify empty labels, absent labels and readonly input behavior. Next use tagged unions to make alternative outcomes explicit.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →