Branded types — a primitive intersected with a marker object — do not compile. This is the usual TypeScript pattern for keeping apart values that share an underlying type, such as a UserId that should not be accepted where a plain string is expected. The marker member exists only in the type system. At runtime the value is the primitive itself.
Version: scriptc 0.0.35, macOS 26.5.1 arm64, Homebrew clang 22.1.4. Also present on main at c41f1a8.
Reproduce
A brand written as an object literal is reported as an index signature problem:
type UserId = string & { readonly __brand: "UserId" };
function toUserId(s: string): UserId {
return s as UserId;
}
const id = toUserId("paolo");
console.log(id);
$ scriptc build brand.ts -o brand
brand.ts:3:10 - error SC2006: values of type 'UserId' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown')
brand.ts:7:7 - error SC2006: values of type 'UserId' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown')
brand.ts:7:12 - error SC2004: uses of 'toUserId' inherit the blocker on its declaration
A brand written with a unique symbol key is reported as an intersection problem:
declare const tag: unique symbol;
type Meters = number & { readonly [tag]: "Meters" };
const d = 5 as Meters;
console.log(d * 2);
$ scriptc build meters.ts -o meters
meters.ts:4:7 - error SC2008: values of type 'Meters' cannot be compiled: this intersection resolves to no runtime shape
meters.ts:5:13 - error SC2004: uses of 'd' inherit the blocker on its declaration
Under Node both programs run and print paolo and 10.
Why it is worth fixing
The two diagnostics come from different parts of the compiler, but the cause is the same. An intersection whose only runtime-carrying part is a primitive has no mapping, so one form falls through to the record path and the other reaches the unresolved-intersection fence.
tests/diagnostics/intersection-values.ts currently treats this shape as unbuildable, describing it as "a primitive part against an object part (inhabited only per the checker, never buildable)". That holds when the object part is a class instance, because no value can be both a number and a class instance. It does not hold for a brand. The marker member has no runtime existence, so a value of UserId is an ordinary string and a value of Meters is an ordinary number. Both can be built, stored, passed and computed with exactly like their unbranded counterparts.
There is also no partial workaround. The brand has to be removed from the type, which gives up the safety it was added for.
Context
Found while compiling a small program that used a branded string for a user name.
Written with AI assistance and reviewed by a human.
Branded types — a primitive intersected with a marker object — do not compile. This is the usual TypeScript pattern for keeping apart values that share an underlying type, such as a
UserIdthat should not be accepted where a plainstringis expected. The marker member exists only in the type system. At runtime the value is the primitive itself.Version: scriptc 0.0.35, macOS 26.5.1 arm64, Homebrew clang 22.1.4. Also present on
mainat c41f1a8.Reproduce
A brand written as an object literal is reported as an index signature problem:
A brand written with a
unique symbolkey is reported as an intersection problem:Under Node both programs run and print
paoloand10.Why it is worth fixing
The two diagnostics come from different parts of the compiler, but the cause is the same. An intersection whose only runtime-carrying part is a primitive has no mapping, so one form falls through to the record path and the other reaches the unresolved-intersection fence.
tests/diagnostics/intersection-values.tscurrently treats this shape as unbuildable, describing it as "a primitive part against an object part (inhabited only per the checker, never buildable)". That holds when the object part is a class instance, because no value can be both a number and a class instance. It does not hold for a brand. The marker member has no runtime existence, so a value ofUserIdis an ordinary string and a value ofMetersis an ordinary number. Both can be built, stored, passed and computed with exactly like their unbranded counterparts.There is also no partial workaround. The brand has to be removed from the type, which gives up the safety it was added for.
Context
Found while compiling a small program that used a branded string for a user name.
Written with AI assistance and reviewed by a human.