Typorithm · hard

14. Branded identifiers and validated constructors

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

14. Branded identifiers and validated constructors

0%

Concept and contract

Prevent accidental identifier mixing without claiming authorization or runtime secrecy.

Two string identifiers may have different meanings even when their runtime representation is identical. A brand adds a type-level distinction so a CompanyId cannot accidentally substitute for a RoleId. This is useful when a project repeatedly passes similarly shaped values through different APIs.

A brand does not encrypt, authorize or validate a value. Restrict brand construction to a small reviewed function that checks the intended syntax or source evidence. Consumers should not scatter assertions throughout the application, because that removes the distinction the brand was meant to preserve.

Mechanism and worked example

The example uses a unique symbol property as a phantom marker and validates a role_ prefix before asserting the branded result.

The example uses a unique symbol property as a phantom marker and validates a role_ prefix before asserting the branded result. The assertion is localized at the point where runtime evidence is established. The marker is not actually added to the primitive string.

Serialization erases the static brand just as it erases other types. A parsed or stored string must pass the appropriate constructor again before trusted use. Keep business ownership checks separate: a syntactically valid id may still refer to another account’s resource.

Complete worked example

typescript

declare const roleBrand: unique symbol;
type RoleId = string & { readonly [roleBrand]: true };
function roleId(value: string): RoleId | undefined {
  return /^role_[a-z0-9]+$/.test(value) ? value as RoleId : undefined;
}
const id = roleId("role_a1");
Reasoning
The assertion is confined to the constructor after a concrete syntax check.

Counterexamples and common mistakes

The rejected assignment mixes CompanyId with RoleId despite their shared primitive representation.

The rejected assignment mixes CompanyId with RoleId despite their shared primitive representation. An ordinary type alias for string would not distinguish them. A brand becomes useful when it prevents a realistic mistake rather than merely making examples look more advanced.

Do not export a generic brand assertion that accepts any string without validation. That turns the constructor into a convenient bypass. Also avoid claiming a prefix check proves existence, freshness or access permission; those require repository lookups and authorization logic.

Rejected counterexample: expected compiler diagnostic

typescript

declare const roleBrand: unique symbol; declare const companyBrand: unique symbol;
type RoleId = string & { readonly [roleBrand]: true };
type CompanyId = string & { readonly [companyBrand]: true };
declare const company: CompanyId;
// @ts-expect-error Different identifier meaning.
const role: RoleId = company;
Reasoning
Distinct phantom markers prevent accidental mixing of equally represented identifiers.

Independent practice

Implement CompanyId and a companyId parser accepting unknown.

Implement CompanyId and a companyId parser accepting unknown. Require a string matching company_ followed by at least one lowercase letter or digit. Return undefined for invalid input. Begin with the concrete validation function before introducing the phantom type.

Test a valid id, a missing suffix, an uppercase suffix and a number. Optional extension: add a repository lookup that checks existence, but keep that asynchronous business rule separate from the basic syntax constructor.

Step 1: Check unknown input is the required string syntax. Step 2: Create the brand only after that check.

Practice starter: solve before reading the solution

typescript

declare const companyBrand: unique symbol;
type CompanyId = string & { readonly [companyBrand]: true };
function companyId(value: unknown): CompanyId | undefined { throw new Error("TODO"); }
Reasoning
Validate unknown input and keep syntax separate from existence or authorization.
  • Hint 1: A syntax check does not authorize resource access.
  • Hint 2: Keep the trusted assertion in one constructor.

Solution, tests and trade-offs

The solution performs a small regular-expression check and localizes the assertion immediately afterward.

The solution performs a small regular-expression check and localizes the assertion immediately afterward. Callers receive either a validated branded string or undefined. The unique symbol is a static distinction, so no extra runtime property is expected during serialization.

Compile a cross-brand assignment rejection and run malformed-input cases. Review the constructor whenever identifier syntax changes. A passing type test cannot detect that a server changed its accepted id format; integration fixtures remain necessary.

Practice solution and acceptance checks

typescript

declare const companyBrand: unique symbol;
type CompanyId = string & { readonly [companyBrand]: true };
function companyId(value: unknown): CompanyId | undefined {
  return typeof value === "string" && /^company_[a-z0-9]+$/.test(value) ? value as CompanyId : undefined;
}

// Acceptance checks
if(companyId("company_a1")!=="company_a1")throw new Error("valid");for(const x of ["company_","company_A",3])if(companyId(x)!==undefined)throw new Error("invalid");
Reasoning
The constructor establishes only the documented syntax and static distinction.

Interview reasoning and next step

In interviews, explain why a string alias is structural and why a phantom unique-symbol property creates a useful distinction.

In interviews, explain why a string alias is structural and why a phantom unique-symbol property creates a useful distinction. Identify the single trusted assertion and its runtime justification. State clearly that a valid brand grants no authorization.

Next, bound recursive transformations. Advanced type machinery is valuable only when its cost and promises remain controlled, especially in public libraries whose consumers have different compiler versions and data models.

Optional stretch — after the core task works: Add an independent existence lookup.

Prevent accidental identifier mixing without claiming authorization or runtime secrecy.