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.
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.
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.
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 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 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
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.
// 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
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
trueExplain 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.
Completion is saved in this browser only. It does not measure mastery.