A strong answer
We intersect the primitive type string with a phantom object type keyed by a unique symbol: declare const __brand: unique symbol; type Brand<T, B extends string> = T & { readonly [__brand]: B }; type TenantId = Brand<string, 'TenantId'>;. (1) Compile-Time Nominal Safety: TenantId and OrderId have incompatible literal types at [__brand] ('TenantId' & 'OrderId' == never), so passing an OrderId to a TenantId parameter is a compile error, and raw strings can only become TenantId through a single validating smart constructor (parseTenantId(raw: string): TenantId). (2) Zero Runtime Cost: TypeScript erases type intersections and casts (raw as TenantId) completely—at runtime, a TenantId is just a raw V8 string pointer (0 wrapper bytes!). (3) Variance: Under strictFunctionTypes, producer return types are covariant (out T) while consumer parameter types are contravariant (in T)—except when declared with method shorthand fn(x: T): void (which is bivariant!), so always use property syntax fn: (x: T) => void for strict contravariant checking.
A value of type Brand<T, B> can only be constructed via an explicit smart constructor or type predicate for brand B, is assignable to base type T without unwrapping, and has 0 bytes of runtime memory overhead over T. Because T & { readonly [__brand]: B } is a subtype of T ($T \cap B \subseteq T$) but T is not a subtype of T & { readonly [__brand]: B }, raw unvalidated T values and values with a different brand $B' \ne B$ are rejected by the structural type checker while erasing to plain T in emitted JavaScript.
Phantom Branded Types add 0 bytes of heap memory and 0 ns of CPU overhead after the single boundary smart constructor check, compared to +32 bytes per ID for runtime wrapper classes (new TenantId(s)). In production systems (Prisma, tRPC, Effect-TS (Brand.nominal), Zod .brand<'TenantId'>(), and TypeScript 4.7+ explicit variance annotations (interface Producer<out T>, interface Consumer<in T>).), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how Phantom Branded Types, Distributive Conditional Types (
infer), and Covariance vs Contravariance operates in memory step by step rather than giving a vague textbook definition. - States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (Phantom Brand (
Brand<string, 'UserId'>), Explicit Variance (interface Box<out T>)) with concrete numbers. - Identifies real-world failure modes (Writing
type IsNever<T> = T extends never ? true : false;and expectingIsNever<never>to betrue) and explains how production systems mitigate them.
Common mistakes
- Treating Phantom Branded Types, Distributive Conditional Types (
infer), and Covariance vs Contravariance as a black box without explaining the underlying memory or state transitions. - Ignoring worst-case tail latency or memory overhead (Using
new Number(x)or wrapper objects to distinguishUsdCentsfromEurCents). - Forgetting boundary conditions and error recovery (Using method syntax
validate(input: T): booleanin a generic validator interface).