Course outline

TypeScript / Basic / Lesson 5

Objects, interfaces, and structural types

Describe object capabilities without confusing a static view with exact runtime shape, ownership, or deep immutability.

About 5 min readContent reviewed
On this page

Lesson goals

  • Define required, optional, and readonly properties.
  • Explain structural compatibility and excess-property checks.
  • Choose interfaces, aliases, and index signatures deliberately.

A type describes the operations a view permits

An object type describes properties and methods that code expects to access. An interface names such a shape and can extend another interface; a type alias can name object types, unions, tuples, and other forms. Neither creates a runtime constructor or removes extra fields from an object. Start with the domain operations and required data, then choose the syntax that keeps the contract understandable. Use small named types at public boundaries so diagnostics and reviews can refer to a meaningful concept rather than a repeated anonymous structure with slightly different fields.

Model absence and restricted access separately

An optional property may be absent. With the course exactOptionalPropertyTypes setting, label?: string permits omission but does not automatically permit writing label: undefined. Reading the property still requires handling possible absence. readonly prevents assignment through that typed access path; it is not a deep runtime freeze or a guarantee that no mutable alias exists. The first example exposes a stable identifier and an optional label. Its formatting policy supplies a fallback only for absence. A valid empty label remains empty because nullish coalescing does not treat empty text as missing.

typescript Optional data with an explicit fallback
export {};
interface Entry { readonly id: number; label?: string }
function display(entry: Entry): string {
  return `${entry.id} ${entry.label ?? "unnamed"}`;
}
console.log(display({ id: 7 }));
Expected output
7 unnamed

The literal omits label. Reading it yields absence and the formatter uses unnamed without changing the object.

Compatibility is based largely on members

TypeScript generally uses structural compatibility: a value can fit an interface when it has the required compatible members, even if it was not created by a class with that interface name. The second example assigns a richer object to a narrower view. Its age field remains present at runtime, although ordinary property access through the narrower type does not expose it directly. This is useful for capability-oriented APIs, but it does not define exact schema validation or strip confidential fields before serialization. Construct an explicit output object when the public representation must contain only selected fields.

typescript A narrower type does not remove runtime fields
export {};
interface Named { name: string }
const person = { name: "Mira", age: 30 };
const view: Named = person;
console.log(view.name, "age" in view);
Expected output
Mira true

view references the same object. Its type exposes name, while the runtime in operator still observes age.

Fresh literals receive additional typo checks

Fresh object literals receive excess-property checks in many assignment and argument contexts, helping detect misspelled option names. This does not mean every object type is exact: assigning a previously inferred richer variable can behave differently. Avoid bypassing a typo with a broad index signature unless arbitrary keys are genuinely supported. An index signature promises a value relationship for matching keys and constrains named properties accordingly. Record<string, number> describes a mapping at the type level but does not manufacture every possible key at runtime. With checked indexing enabled, callers must handle missing values from uncertain access.

Make copying and serialization deliberate

Object spread creates a shallow copy, so nested objects can still be shared. readonly properties likewise do not make nested values deeply readonly unless the type is designed that way. For configuration updates, define which fields callers may change and validate those changes before committing. For API output, build a selected representation rather than asserting a private object to a public interface and serializing the original. Interface extension and declaration merging can be useful library mechanisms, but accidental global declarations can change unrelated files. Keep examples and application files in modules when shared global augmentation is not intended.

Common errors and fixes

An optional property cannot be assigned undefined.

Why it happens
Exact optional semantics distinguish omission from explicit undefined.
How to fix it
Omit the field or deliberately include undefined in its type.
Prevent it next time
Specify patch and serialization semantics explicitly.

Private fields appear in serialized public output.

Why it happens
A narrow type view was mistaken for field removal.
How to fix it
Construct an explicit public object before serialization.
Prevent it next time
Test actual output keys, not just assignability.

A readonly nested object still changes.

Why it happens
The restriction is shallow or another alias mutates it.
How to fix it
Design the required immutable view and runtime ownership policy.
Prevent it next time
Review aliasing independently from interface syntax.

Check your understanding

1. Call display with id seven and an empty label. Predict the result and contrast it with a fallback using ||.

Show worked solution

The returned text is "7 " with a trailing space because the empty string is present and ?? preserves it. Using || would replace the empty string with unnamed. Decide which behavior the domain wants instead of treating the two operators as interchangeable. Test omission and a valid empty string as separate cases.

2. A user object contains name and secretToken, and a public interface lists only name. Describe a safe output construction and its runtime test.

Show worked solution

Construct a new object such as { name: user.name } and serialize that selected object. Assert in a runtime test that the resulting keys contain only name and that secretToken is absent. Merely assigning user to the public interface keeps the original fields and is not a security filter. Authorization rules must also be enforced at the actual access boundary.

Key takeaways

  • Object types are views of capabilities, not sanitizers.
  • Optionality, readonly access, and ownership differ.
  • Test actual keys and nested mutation when they matter.

Official references