Skip to main content

Values and types

Rank values are immutable. Once a value is produced it cannot be changed. There is no mutation, no variable reassignment, and no mutable state anywhere in the language.

Primitive values

KindExamples
string`hello`, `port: 8080`
number42, 3.14, -1
booltrue, false
nullnull

String literals use backticks as their only delimiter. They may span multiple lines and support ${...} interpolation when each interpolated expression has type string:

name = `world`
greeting = `Hello, ${name}!`

Regular expression literals use the re prefix:

pattern = re`foo-[0-9]+`

Structured values

Lists are ordered sequences:

ports = [80, 443, 8080]
names = [`alice`, `bob`, `carol`]

Objects are ordered maps of named fields:

config = {
host: `localhost`,
port: 5432,
}

Field order is preserved in emitted output. There is no implicit reordering.

Type aliases

Types are declared with leading-uppercase names. The bare Name = Type form creates a type alias:

Config = Object {
host: string,
port: number,
}

StringList = [string]

Generic type aliases use angle brackets. Parameters may be plain names, bounded with extends, or trailing-defaulted with =:

Box<T = string> = Object {
value: T,
}

Pair<A, B> = Object {
left: A,
right: B,
}

Field<T, K extends keyof T = `name`> = T[K]

Bounds and defaults are resolved left to right, so later parameters may reference earlier ones, but not the other way around. Omitted type arguments are only allowed for the trailing defaulted suffix, so a bare Box reference is valid while Pair<string> is not.

Recursive generic aliases are supported when recursion comes back to the same alias with the same type arguments:

Tree<T> = Object {
value: T,
left: [Tree<T>],
right: [Tree<T>],
}

LinkedList<T> = null | Object {
head: T,
tail: LinkedList<T>,
}

Changing-argument recursion is not supported:

Grow<T> = Object {
next: Grow<[T]>,
}

Type aliases live in the type namespace. They cannot be used as value bindings.

Parse template string types

Structured string formats can be captured as types with parse templates:

Version = parse`{major:number}-{minor:number}-{patch:number}`

version: Version = `1-2-3`

A parse template type matches only strings whose full text matches the template and whose named segments decode to the requested types.

This is a core language feature, not a server-only trick or a standard-library parser helper. Rank uses the same structured-text idea in three places:

  • parse template types such as a Version = parse ... alias
  • parse patterns inside match expressions
  • route matching forms such as HTTP::Route path patterns with the match prefix

Use a parse template type when you want to say “only strings of this shape are valid here.” A value of type Version is still just a string at runtime; the type constrains which strings are allowed.

When you want to bind the decoded pieces, use the same parse syntax as a pattern:

majorOf = |text: string| -> number | null {
return match text {
parse`{major:number}-{minor:number}-{patch:number}` => major,
_ => null,
}
}

These types are useful for validated version strings, route fragments, tagged identifiers, and other structured text values. For the pattern-matching side of the language, see Bindings and functions.

Object types

Object types are closed by default — an object with extra fields does not match a closed type:

Config = Object {
host: string,
port: number,
}

Open object types accept additional fields beyond the declared ones:

// accepts any object with at least a `host` string field
HostConfig = Object {
host: string,
...: string, // additional fields may be any string-valued key
}

Optional fields use ?:

ServerConfig = Object {
host: string,
port: number,
tls?: bool, // may be absent
}

Reading an absent optional field yields null. Use ?? for an explicit default:

cfg: ServerConfig = { host: `localhost`, port: 443 }
use_tls = cfg.tls ?? false

Field annotations on object types

Ordinary object-type fields and refinement fields can carry @constraint, @unique, and @sensitive directly on the field definition:

use std::string::{ isBlank }

ServiceConfig = Object {
@unique(group: service_ids)
id: string,
@constraint(cond: |self| !isBlank(self))
name: string,
@constraint(cond: |self| self >= 1)
replicas: number,
@sensitive
apiToken?: string,
}

These annotations travel with the type:

  • self inside a field constraint is the field value.
  • Absent optional fields skip field validation and unique registration.
  • Refinements inherit field annotations by default.
  • Pick<T, K>, Partial<T>, and Required<T> preserve field annotations when the field survives.
  • Omit<T, K> removes them with the field, and Record<K, V> creates fresh fields with no inherited annotations.
  • Mapped bodies and rest fields do not accept field annotations in the current language surface.

Type unions and intersections

Union types accept either shape:

StringOrNumber = string | number
Mode = `dev` | `prod` | `staging`

Intersection types require both shapes:

Named = Object { name: string }
Versioned = Object { version: number }
NamedAndVersioned = Named & Versioned

Type queries and utility types

keyof produces the field-name union for an object type, and indexed access reads a field type from another type:

User = Object {
profile: Object {
displayName: string,
},
}

UserKeys = keyof User
DisplayName = User[`profile`][`displayName`]

These operators are the building blocks for reusable type helpers. You can write constrained aliases that only accept valid object keys:

FieldOf<T, K extends keyof T> = T[K]

Profile = FieldOf<User, `profile`>
DisplayName = Profile[`displayName`]

The extends clause is a declaration-site constraint on the type parameter. Here it means K must be assignable to keyof T. If a call site instantiates FieldOf<User, missing>, the compiler reports a type-argument constraint error at the alias boundary.

Common reusable type transforms live under std::types:

use std::types::{ Pick, Omit, Partial, Required, Record, Exclude, Extract, NonNullable }

User = Object {
id: string,
name: string,
email?: string,
passwordHash: string,
}

Summary = Pick<User, `id` | `name`>
PublicUser = Omit<User, `passwordHash`>
Draft = Partial<User>
CompleteDraft = Required<Partial<User>>
Flags = Record<`featureA` | `featureB`, bool>
TextOnly = Extract<string | number, string>
NumberOnly = Exclude<string | number, string>
Email = NonNullable<User[`email`]>

Pick and Omit constrain their key parameters to keyof T, so invalid keys fail at alias instantiation rather than later in mapped-type evaluation.

The utility types fall into a few common groups:

Utility familyTypesWhat they do
Object field selectionPick, OmitKeep or remove named fields from an object type. The key parameter is constrained to keyof T.
Object optionalityPartial, Required, DeepPartialMake fields optional, required, or recursively optional.
Object constructionRecordBuild an object type from a key union and a value type.
Union filteringExclude, Extract, NonNullableRemove or keep union members based on assignability.

These are ordinary compile-time aliases, not runtime functions. They can be composed with keyof, indexed access, conditional types, and your own constrained aliases.

DeepPartial<T> recursively makes nested object fields optional, including nested object values inside lists:

use std::types::{ DeepPartial }

Config = Object {
nested: Object {
count: number,
},
items: [Object {
label: string,
}],
}

draft: DeepPartial<Config> = {
nested: {},
items: [{}],
}

These utilities operate entirely at compile time. They refine how the compiler checks values; they do not add runtime fields or emitted output.

When a utility type preserves a field, it also preserves that field's annotations. This is what lets reusable constraints, uniqueness groups, and sensitive redaction survive through views such as Pick<T, K> and Partial<T>.

Type annotations on bindings

Annotate a binding with : Type to ask the compiler to verify the type:

port: number = 8080
config: Config = { host: `localhost`, port: 5432 }

If the value does not match the declared type, the compiler reports an error before evaluation.

Function types

Function types are written with || for the parameter list and -> for the return type. They describe the shape of a function value:

Transform = |number| -> number
Predicate = |string| -> bool
BinaryOp = |number, number| -> number

Zero-argument functions:

Thunk = || -> string

A function that matches a type alias must have compatible parameter types and return type:

Transform = |number| -> number

double: Transform = |x| -> number {
return x * 2
}

null semantics

null is an explicit value for interop and absence. It is distinct from a missing optional field:

value: string | null = null

?? is the null-defaulting operator. It evaluates the right side only when the left is null:

result = potentially_null_value ?? `default`

Functions must explicitly handle null. Automatic lifting does not skip null inputs.