arthrop0d

Brief articles on varied topics.

TypeScript Structural Assignability and Excess Properties

typescriptsoftware engineering

TypeScript uses structural assignability: an object can be used where another object type is expected when it has the required shape, regardless of the name of its type. It also applies excess-property checking to object literals, a stricter check intended to catch misspelled or unintended fields.

That combination explains a familiar surprise: passing an object literal directly can produce an error, while putting the same value in a variable first is accepted.

The problem structural typing solves

In a nominal type system, compatibility usually depends on an explicit declaration or relationship: a value of Dog is accepted as a Dog, but an unrelated type with identical fields might not be. TypeScript instead models many JavaScript values by their members. This is useful because JavaScript commonly uses objects as records, options, and lightweight interfaces without requiring classes or registration.

interface Point {
  x: number;
  y: number;
}

function distanceFromOrigin(point: Point): number {
  return Math.sqrt(point.x ** 2 + point.y ** 2);
}

const location = { x: 3, y: 4, label: "office" };
distanceFromOrigin(location); // accepted

location has at least the members that Point requires, with compatible types. Its additional label member does not prevent it from being used as a Point. The function only depends on x and y, so structural assignability reflects what the function actually needs.

A type is assignable to another type when TypeScript permits a value of the first type in a position requiring the second. For object types, the important direction is that the source must provide every required property of the target, and each provided property must have a compatible type.

const incomplete = { x: 3 };
distanceFromOrigin(incomplete); // error: y is missing

const wrongKind = { x: "3", y: 4 };
distanceFromOrigin(wrongKind); // error: x is not a number

The extra check on literals

Now compare a variable with a direct object literal:

interface Options {
  timeout: number;
}

function connect(options: Options) {
  // use options.timeout
}

const configured = { timeout: 1000, tiemout: 1000 };
connect(configured); // accepted by structural assignability

connect({ timeout: 1000, tiemout: 1000 });
// error: 'tiemout' does not exist in type 'Options'

The second call uses an object literal, an object value written directly with braces rather than referred to through a variable. TypeScript performs excess-property checking on such a literal. It notices that tiemout is not a property of Options and reports it.

This is deliberately not a general rule that says extra properties are always invalid. It is a targeted safeguard for a common mistake: a misspelled option, a copied field intended for another API, or a property that the receiving function will silently ignore. Without the check, tiemout could look like a configured timeout while having no effect.

The variable case is accepted because ordinary structural assignability asks a narrower question: Can this existing value provide what connect needs? It can provide timeout, so the answer is yes. TypeScript does not generally require every value to have exactly the target's properties; doing so would make normal JavaScript patterns unnecessarily difficult.

Why the context changes the result

A fresh object literal is treated specially because TypeScript can inspect the complete value at the point where it is created. The literal is also contextually typed, meaning the expected type helps TypeScript check the literal's property names and property value types.

const good: Options = { timeout: 1000 };
const typo: Options = { tiemout: 1000 }; // error

The annotation gives the literal an expected shape, so the unknown property is caught. Passing the literal directly as an argument provides the same kind of context: the function parameter's type is the expected shape.

Assigning first removes that particular context:

const value = { tiemout: 1000 };
connect(value); // error: timeout is missing

This still fails, but for a different reason: the variable does not contain the required timeout property. If it contains timeout plus an extra property, as in the earlier example, structural assignability succeeds.

People sometimes describe the literal as “fresh.” This means, informally, that it is a newly written object literal whose properties are available for the extra-property check. Once the value is stored in a variable, TypeScript treats the variable's inferred object type as the source type for an ordinary assignability comparison. The value has not changed at runtime; only the type-checking situation has.

Making the intent explicit

If a value is meant to conform to an options type and you want its fields checked at its declaration, annotate it:

const options: Options = {
  timeout: 1000,
  tiemout: 1000, // caught here
};

TypeScript also provides the satisfies operator for checking that an expression conforms to a type while preserving the expression's more specific inferred type:

const options = {
  timeout: 1000,
} satisfies Options;

Use an index signature when arbitrary named properties are genuinely part of the contract, rather than suppressing a useful error. For example, a type with [key: string]: unknown explicitly says that other string-named properties are allowed; it changes the contract instead of bypassing the check.

The practical rule is therefore simple: structural assignability permits values with at least the required shape, while excess-property checking scrutinizes object literals for unexpected fields. When the two appear inconsistent, TypeScript is distinguishing a general existing value from a freshly written literal where a likely typo can still be identified.

← All articles