You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This suggestion and a working prototype were drafted with assistance from Cursor (AI coding tools). I have read the design and the prototype, understand the intended behavior, and will shepherd discussion and any follow-up myself.
Problem
Today there is no first-class way to get all four of:
Nominal separation of two brands with the same members
Block widened string / number
Accept trusted member literals at call sites (setRole("admin"))
This proposal does not try to solve general nominal / opaque typing (#202). It targets one concrete gap: finite opaque sets with ergonomic literals.
Suggested solution (normative = Mode A)
brandtypeAccountType="admin"|"regular";
Declares:
TypeAccountType — nominal over the literal union
ValueAccountType with generated:
AccountType.is(value: string): value is AccountTypeAccountType.from(value: string): AccountType// throws TypeError if !is
Mode A constraints (phase 1):
RHS is a finite union of string / number / bigint literals
No open string / number, intersections, or conditionals in Mode A
Distinct brand names are not mutually assignable even with identical members
Examples
brandtypeAccountType="admin"|"regular";brandtypeOtherRole="admin"|"regular";declarefunctionsetRole(role: AccountType): void;declareconstraw: string;setRole("admin");// OK — member literalsetRole("regular");// OKsetRole("superuser");// Error — not in the setsetRole(raw);// Error — widened stringsetRole(AccountType.from(raw));// OKif(AccountType.is(raw)){setRole(raw);// OK — CFA}declareleta: AccountType;declareletb: OtherRole;a=b;// Error — different brands
Generated JavaScript (declaration only)
Expressions are not rewritten based on types. "admin" stays "admin" at call sites.
Philosophy note (vs PR #33038): Mode A’s invariant is decidable membership in a finite literal set — the checker already understands that for unions. Attaching a nominal tag when membership is known does not invent new proofs. Open brands (brand type Email = string) must not auto-accept arbitrary literals; that is explicitly out of Mode A.
Phase 2 (optional, separable) — refined brands
Same dual, custom is, generated from, conservative assignability (no bare number → brand):
brandtypePositiveInt=number{is(n: number): n is PositiveInt{returnNumber.isInteger(n)&&n>0;}}
Mode B can be rejected without killing Mode A. Libraries already approximate Mode B; Mode A is the part libraries cannot do without casts or a transformer (setRole("admin") on a nominal brand).
Working prototype (POC, not a merge request)
Exploratory implementation on a fork of the native (Go) compiler:
AI disclosure
This suggestion and a working prototype were drafted with assistance from Cursor (AI coding tools). I have read the design and the prototype, understand the intended behavior, and will shepherd discussion and any follow-up myself.
Problem
Today there is no first-class way to get all four of:
string/numbersetRole("admin")).is/.from)string"admin" | "regular"enumT & { __brand }brand type(Mode A)Related prior art / discussion: #202, #4895, #17690, PR #33038.
This proposal does not try to solve general nominal / opaque typing (#202). It targets one concrete gap: finite opaque sets with ergonomic literals.
Suggested solution (normative = Mode A)
Declares:
AccountType— nominal over the literal unionAccountTypewith generated:Mode A constraints (phase 1):
string/number, intersections, or conditionals in Mode AExamples
Generated JavaScript (declaration only)
Expressions are not rewritten based on types.
"admin"stays"admin"at call sites.Why not “another enum”?
enumbrand type.is/.fromPhilosophy note (vs PR #33038): Mode A’s invariant is decidable membership in a finite literal set — the checker already understands that for unions. Attaching a nominal tag when membership is known does not invent new proofs. Open brands (
brand type Email = string) must not auto-accept arbitrary literals; that is explicitly out of Mode A.Phase 2 (optional, separable) — refined brands
Same dual, custom
is, generatedfrom, conservative assignability (no barenumber→ brand):Mode B can be rejected without killing Mode A. Libraries already approximate Mode B; Mode A is the part libraries cannot do without casts or a transformer (
setRole("admin")on a nominal brand).Working prototype (POC, not a merge request)
Exploratory implementation on a fork of the native (Go) compiler:
feature/brand-type-mode-abrandTypeModeA_*,brandTypeModeB_*undertsc/testdata/tests/cases/compiler/Happy to adjust design to feedback; treating the fork as a feasibility check, not a finished PR.
Open questions
brand typevs something aligned with Support some non-structural (nominal) type matching #202 (unique/opaque)?.from: alwaysTypeError?const-likeSearch terms
brand type, finite literal brand, nominal string union, literal assignability, companion.is.from, not enum, not full #202