Compatibility compares required structure
For simple objects with public properties, compatibility checks whether the source supplies members required by the target with compatible types. A variable with id and attempts can be assigned to a view requiring only id. Type names do not by themselves create a nominal hierarchy or a transformation operation.
Assignment still refers to the same object. It does not delete attempts, copy data or choose what will be serialized. This distinction matters at public boundaries: reducing the type visible to a consumer does not automatically reduce the runtime payload. Explicit projection may be necessary even when static compatibility passes.
Fresh literals receive an additional check
A literal directly assigned to Config undergoes excess-property checking. A retires field when the contract declares retries can be reported as a likely spelling error. This helps find configuration-object mistakes, where an ignored option may have consequences difficult to observe.
First storing the literal in a variable can change that checking, but it fixes neither the name nor the extra field. An assertion can also bypass a diagnostic without improving the contract. If additional fields are genuinely allowed, they need an explicit policy. If a known option was intended, correct the property rather than hide the warning.
Absence is not always explicit undefined
With exactOptionalPropertyTypes: true and strict: true, theme?: "light" | "dark" admits property absence and declared values when present. It does not automatically add undefined to values that may be written. If the contract needs that present state, it must declare it.
Reading may still obtain undefined when the property is absent. The option therefore does not remove the need for consumers to handle absence. The distinction can matter in partial updates: not sending a key and sending a key with undefined need not be the same decision before serialization. Protocol policy should be defined and reflected in the type.
readonly restricts an access path
A readonly property cannot be reassigned through that type during checking. That does not imply runtime freezing. If another variable references the same object with mutable access, changes through that variable can be observed in the readonly view.
The modifier is not automatically deep either. In readonly policy: { limit: number }, the policy reference cannot be replaced through that view, but limit remains mutable. A snapshot requires deciding which levels are copied and which typed accesses are restricted. Copying only the outer object leaves children shared; freezing the source can conflict with the owner’s need to keep working.
Intersection combines requirements rather than replacing fields
The & operator requires a value to satisfy combined contracts simultaneously. If both declare value, one as string and the other as number, the field would need to meet both requirements. This produces never for the field rather than choosing the last type or creating a union. It is not the same operation as constructing an object with spreads and overriding a property.
An interface extending another must also retain compatible members. Replacing textual id with numeric id requires a boundary decision and consideration of old consumers. A separate type and explicit adapter can express that difference better than an impossible intersection or an assertion pretending safe substitution.
Homogeneous maps and separate metadata
A [key: string]: number signature states that covered property values are numeric. A label: string property in the same object contradicts that requirement. Widening everything to number | string removes the conflict but also permits text in values that may need to remain exclusively numeric.
If the contract requires text metadata and counts by key, a structure separating label from counts can retain both rules. That structure does not prove a particular key exists: absence and indexed-read options are addressed in the collections module. Here the decision concerns present values and separation between the collection’s name and its homogeneous data.
Guided reading of projection and snapshot
capture constructs a new object with known fields, a new policy and a new tags array. label, limit and tags elements are primitive values in the shown schema. Later owner changes at mutable levels are therefore not shared with the result. If the schema gains objects inside tags, the strategy needs review.
The Snapshot type restricts writes by typed consumers but creates no universal guarantee against JavaScript code or incorrect assertions. In the cases, separate two needs: limiting transmitted fields and retaining approval-time values. The first requires projection; the second requires reference analysis. A narrower annotation performs neither operation.
// TypeScript 7.0; strict: true; plain data, complete schema below.
type Owner = {
label: string;
policy: { limit: number };
tags: string[];
};
type Snapshot = {
readonly label: string;
readonly policy: { readonly limit: number };
readonly tags: readonly string[];
};
function capture(owner: Owner): Snapshot {
return {
label: owner.label,
policy: { limit: owner.policy.limit },
tags: [...owner.tags]
};
}Read the references created by capture. Copying separates policy and tags from the owner for this simple-value schema. readonly restricts typed snapshot access but neither executes Object.freeze nor validates external data.
Common pitfalls
Confusing a typed view with projection; treating readonly as deep freezing; ignoring absence versus undefined; using intersection as property replacement.
Related topics: Static types and runtime values · Mapped types and conditional models · Collections, nullability and mutation
The static contract and actual object are related but distinct. Publication shape and reference sharing require explicit decisions.
Reference: TypeScript: Structural compatibility · TypeScript 7.0; explicit strict and host configuration