Structural Contracts

The Course
•
5 min read

This course builds on TypeScript beginner: unions, narrowing, readonly records, small generics, unknown-data validation and the local project. Days 5–7 also use JavaScript promises, async/await and injected service dependencies. JavaScript advanced is not required. Refresh those linked topics before Day 5 if needed.

Our goal is a typed catalogue boundary: external values become validated books, a typed transport supplies data, and a reducer ignores stale completions. The compiler will enforce relationships, while runtime checks enforce data truth and timing.

Structural compatibility means a value may have more fields than a parameter asks for. Returning it under a smaller type does not remove those fields. If a public view must exclude secret data, construct a new object with only allowed properties. Assertions and Pick are not sanitizers. First predict the key difference below; it is the same distinction behind the corrected update exercise in beginner.

Before you start

Use this beginner-level warm-up to check your footing. Explain why an unknown value needs narrowing and why the function returns a union. Review unknown values and narrowing if you cannot explain both branches yet. You do not need async JavaScript until Day 5.

Prerequisite warm-upTypeScript
function titleOf(value: unknown): string | null {
  if (typeof value !== "object" || value === null || !("title" in value)) {
    return null;
  }

  return typeof value.title === "string" ? value.title.trim() : null;
}

console.log(titleOf({ title: " Types " }) ?? "Invalid");
console.log(titleOf({ title: 25 }) ?? "Invalid");

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
Invalid

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Use Check types first; execution is allowed only when the real compiler accepts the source.

Predict, then runTypeScript
type User = {
  id: string;

  name: string;

  secret: string;
};

type PublicUser = Pick<User, "id" | "name">;

const user: User = { id: "u1", name: "Ada", secret: "private" };
const merelyTyped: PublicUser = user;
const projected: PublicUser = { id: user.id, name: user.name };

console.log(Object.keys(merelyTyped).join(","));
console.log(Object.keys(projected).join(","));

Enable JavaScript to edit and type-check this example.

Check your prediction
id,name,secret
id,name

Read the compiler error

Read the property mismatch. Structural compatibility still requires each promised field to have a compatible type.

Find and fix the type errorTypeScript
type User = {
  id: string;

  name: string;
};

const user: User = { id: "u1", name: 25 };

console.log(user);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Use a string name, or intentionally change the domain contract. Casting would not turn a number into text.

Type compatibility and data projection are separate contracts

Structural compatibility lets a richer value pass through a narrower interface. That is useful for reusable functions, but it does not filter the runtime object. If a caller must not receive internal fields, create a new public record rather than returning the original under a smaller annotation.

Ownership is another independent promise. A new outer object can still share nested data. State whether a public function returns a view, a shallow projection or an isolated snapshot, and test the actual keys and references that promise covers.

Type compatibility and data projection are separate contracts: traceTypeScript
type Public = { title: string };

const source = { title: "Types", secret: true };

function view(value: Public): Public {
  return value;
}

console.log(Object.keys(view(source)).join(","));

Enable JavaScript to edit and type-check this example.

Check your prediction
title,secret

The function has a valid parameter and return type. It still returns the original object, including data the public annotation does not expose through ordinary property access.

Type compatibility and data projection are separate contracts: contrastTypeScript
type Public = { title: string };

const source = { title: " Types ", secret: true };

function project(value: Public): Public {
  return { title: value.title.trim() };
}

const result = project(source);

console.log(Object.keys(result).join(","));
console.log(source.title === " Types ");

Enable JavaScript to edit and type-check this example.

Check your prediction
title
true

The body implements projection and normalization while leaving the source unchanged. The smaller alias alone implemented neither behavior.

Try the rule on new inputs

Test a source with extra fields and a padded title. Compare returned keys, source values and object identity. Explain which checks remain necessary even when the public return annotation is correct.

Your turn

Exercise

Structural Contracts: practise

Write publicBook(book) returning only id and title from a Book containing id, title and internalNote. Print id,title for the returned keys, and true that the original still has internalNote. Do not spread the whole input.

Your workspaceTypeScript
// Name the full and public shapes, then explicitly project the allowed fields.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

A smaller return annotation alone does not transform the object.

Hint 2: a first step

Return an object literal naming just id and title.

Show one solution
One possible solutionTypeScript
type Book = {
  id: string;

  title: string;

  internalNote: string;
};

type PublicBook = Pick<Book, "id" | "title">;

function publicBook(book: Book): PublicBook {
  return { id: book.id, title: book.title };
}

const original: Book = { id: "b1", title: "Types", internalNote: "private" };

console.log(Object.keys(publicBook(original)).join(","));
console.log("internalNote" in original);

Enable JavaScript to edit and type-check this example.

id,title
true

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Change the implementation to return book directly: it still type-checks, but the key expectation should fail. Restore projection. Explain why external JSON must be validated separately even if its caller-facing type looks correct.

Was this article helpful?

Your response is saved on this device.