Structural typing normally treats two strings alike, even if one represents a book id and another represents a user id. An intersection can add a compile-time marker: BookId is a string AND a record with a private brand key. This is the ampersand’s meaning in a type: both requirements must hold.
Understand the bracketed key
A JavaScript symbol is a distinct property key. Symbol("tag") creates one; the label is for debugging, not a string key. In an object literal, brackets compute a property name from a value. Reading the same bracketed key retrieves it.
const tag = Symbol("tag");
const record = { [tag]: "book" };
console.log(record[tag]);
console.log("tag" in record);Enable JavaScript to edit and type-check this example.
Check your prediction
book
false The brand below uses similar bracket syntax only in a type. It does not add a symbol property to a string. Read its pieces separately: declare introduces a checking-only name, unique symbol gives that name a distinct key type, and readonly prevents assignments through that marker type.
declare const bookIdBrand: unique symbol declares a distinct symbol type for checking; declare emits no JavaScript variable. We use that symbol only as a computed type key, never as a runtime property. The string returned by the factory is still an ordinary string at runtime.
The factory below is the one audited assertion boundary. It first checks the exact rule—nonblank text after trimming—then asserts that validated string as BookId. The assertion does not perform the check; the preceding code is the evidence. Export the type and factory from a module while keeping its brand key private.
A brand prevents accidental mixing by cooperative TypeScript callers. It is not authorization, a secret token, a guarantee against casts, or proof that a book exists in a database. Keep its claimed meaning narrow and test the factory’s normalization and rejection rules.
Predict, check, and run
Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.
declare const brand: unique symbol;
type BookId = string & { readonly [brand]: "BookId" };
function bookId(value: unknown): BookId | null {
if (typeof value !== "string" || value.trim() === "") {
return null;
}
return value.trim() as BookId;
}
console.log(bookId(" b1 "));
console.log(bookId(" "));Enable JavaScript to edit and type-check this example.
Check your prediction
b1
null Read the compiler error
Predict the rejected relationship before checking. Read the real diagnostic and its source location; fix the contract or the value instead of hiding it with an assertion.
declare const brand: unique symbol;
type BookId = string & { readonly [brand]: "BookId" };
const id: BookId = "b1";
console.log(id);Enable JavaScript to edit and type-check this example.
Check your diagnosis
A plain string has not satisfied the branded contract. Obtain it from the checked factory; do not scatter assertions around call sites.
Build it yourself
Branded IDs: practise
Create a branded UserId and a checked factory returning UserId|null. Accept and trim ” u1 ”, reject 25 and blanks. Print u1 and true for rejection. Keep the assertion only after validation.
// Start from the stated contract. Add a valid call and an edge case.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
The brand marker is type-only; runtime evidence must come from the factory body.
Hint 2: a first step
Narrow to string, trim once, reject empty text, then assert the normalized value.
Show one solution
declare const userIdBrand: unique symbol;
type UserId = string & { readonly [userIdBrand]: "UserId" };
function userId(value: unknown): UserId | null {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
if (normalized === "") {
return null;
}
return normalized as UserId;
}
if (userId(" ") !== null) {
throw new Error("Blank id accepted");
}
console.log(userId(" u1 "));
console.log(userId(25) === null);Enable JavaScript to edit and type-check this example.
u1
trueTrace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.
Change it and test it
Create separate BookId and UserId types and try passing one where the other is required. Then remove the factory validation and explain why the cast still compiles while runtime tests expose the lie.
Completion is saved in this browser only. It does not measure mastery.