Model domain values with defined types
Use type Name = distinct T when two values share a runtime representation but should not be mixed accidentally. Use alias only when a second spelling should remain fully interchangeable.
Source
import go fmt from "fmt";
type UserID = distinct string;
type Scores = distinct Map<UserID, int>;
alias DisplayName = string;
public function label(this: UserID): string {
return "user:" + string(this);
}
function score(values: Scores, id: UserID): int {
const [value, present] = values[id];
if (!present) { return -1; }
return value;
}
function main(): void {
const id = UserID("42");
const name: DisplayName = "Aki";
const values = Scores(makeMap[UserID, int]());
values[id] = 100;
fmt.Println(id.label(), name, score(values, id));
fmt.Println(score(values, UserID("missing")));
}Run
keika check defined-types.km
keika run defined-types.kmExpected output:
user:42 Aki 100
-1Defined type versus alias
UserID and Scores are new nominal types. Their compatible underlying values cross the boundary with explicit conversions such as UserID(raw) and string(id). UserID keeps string operations between two UserID values and is comparable, so it is a valid map key.
DisplayName is a transparent alias for string; no conversion is needed. It communicates intent but does not prevent mixing.
The label receiver is declared in the same module as UserID, giving the defined type its own method set. Imported types cannot be extended this way.
The documentation suite also verifies that returning a raw string as UserID is rejected. This protects the domain boundary rather than silently converting it.