Skip to content

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 ​

ts
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 ​

sh
keika check defined-types.km
keika run defined-types.km

Expected output:

text
user:42 Aki 100
-1

Defined 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.

See Type-system reference.

Kinmokusei is a pre-1.0 project. Documentation describes implemented behavior.