Typed Literals
You’ve already met one of these: Date"2026-06-15" in Dates & Times
isn’t a separate language feature, it’s a regular type-qualified function call dressed up as
a literal. The grammar is <Type>"…" for any type in scope, and the body
becomes a list of fragments — alternating literal text and ${…}
interpolations — that the type’s handler reduces into a value.
The shape
Section titled “The shape”A typed literal’s prefix is a type that implements the Literal interface —
one function, from_fragments(List<Fragment<'I>>) -> 'R, with no self
parameter. The type’s own name prefixes the literal at use sites; I is the
interface every ${…} slot must satisfy, and R is what the literal produces. A Sql"…" site
desugars to Sql.from_fragments([...]) — the ordinary interface dispatch
you’ve seen for Display/Debug, so coherence and the orphan rule apply
unchanged:
import {
std/io: IO
std/literal: Fragment, Literal
}
type SafeQuery String
// A zero-sized type whose only job is to name the literal.
type Sql
impl Literal for Sql {
fn from_fragments(fragments: List<Fragment<String>>): SafeQuery {
body = Iter.reduce(fragments, |acc = "", frag|
case frag {
.Static(s) -> acc + s
.Dynamic(v) -> acc + "'" + v + "'"
}
)
SafeQuery(body)
}
}
fn main() {
user = "alice"
q = Sql"SELECT * FROM users WHERE name = ${user}"
SafeQuery(text) = q
IO.print(text)
}
The body of Sql"…" is split into Static(text) runs and Dynamic(value)
slots wherever ${…} appears. The handler walks the list and builds the
result however it likes — here we add SQL-style quotes around dynamic
values, but a real implementation might validate, parameterize, or escape.
The prefix is just a type
Section titled “The prefix is just a type”Because the prefix is a type, you have two natural ways to spell one:
- A zero-sized
type Sql, as above — its only purpose is to name the literal, giving a DSL a dedicated stateless value type. - An existing domain type, so the use site reads as a constructor.
Box"…"builds an actualBox— same spelling whether you writeBox{contents: "x"}orBox"x":
import {
std/io: IO
std/literal: Fragment, Literal
}
struct Box {
field contents: String
}
impl Display for Box {
fn to_string(b: Box): String {
"Box(${b.contents})"
}
}
impl Literal for Box {
fn from_fragments(fragments: List<Fragment<String>>): Box {
body = Iter.reduce(fragments, |acc = "", frag|
case frag {
.Static(s) -> acc + s
.Dynamic(v) -> acc + v
}
)
Box{contents: body}
}
}
fn main() {
IO.print(Box"hello")
IO.print(Box{contents: "world"})
}
Box"hello" and Box{contents: "hello"} build the same value (an actual
Box), through different syntactic doors.
The next chapter — FFI & Dynamic — covers how Nomi
programs call out to the current Go host through extern fn declarations, and
how the Dynamic type handles untyped values at that boundary.