Skip to content

Types

E# has a small set of type kinds. The split that matters most is value vs. identity: data is value-semantic by default, ref data is the object world you opt into.

data is the kind you reach for most. It carries a value-semantic contract:

  • Copy-on-assignlet b = a copies; mutating b never touches a.
  • No object identity — two data values with equal fields are equal; there’s no reference identity.
  • Value-shaped equality — field-wise by default.
  • No shared mutation through aliases — every binding is its own value.
  • nil is invalid on plain T — nullability requires *T or T?.
  • The CLR form isn’t part of the contract — the compiler represents a data as a struct or a class (small/simple stays a struct; large/reference-heavy/heavily-copied silently becomes a class). The contract holds either way, so you never have to care; a C# consumer sees a stable surface regardless. Pin it with [StackAlloc] / [HeapAlloc] when you have a measured reason.
data Point { x: int, y: int }
let a = Point { x: 1, y: 2 }
let b = a // a copy — mutating b never touches a

To a C# eye, data reads a lot like a record — value equality, with, copy semantics — but record is a C#-language construct, not a CLR primitive. data is E#‘s own take: it lowers to a plain CLR struct or class, with the value-equality and with machinery generated directly.

Fields can be written on one line (comma-separated) or one per line. Construction is the composite literal T { field: value }; there are no constructors on data — an init block on a data is an error (ES3012). If construction needs logic, write a factory function:

func makePoint(x: int, y: int) -> Point = Point { x: x, y: y }

A data can’t contain itself by value (that would be infinitely large) — break the cycle with a pointer (ES2002). See Pointers & memory.

data Node { value: int, next: *Node } // *Node, not Node
data Tree { value: int, children: List<*Tree> } // through a generic container too

Declaration order is free. A type may reference another declared later in the file, and two types may reference each other — the compiler resolves all type names regardless of order:

data Field { key: string, value: Json } // references Json, declared below
ref choice Json {
jnull
jobj(fields: List<Field>) // ...which references Field — a mutual cycle
}

Fields are mutable by default. Mark a field let (immutable after construction — emits initonly) or var (explicitly mutable):

data Cursor { var position: int, let label: string }

Sugar for the full form plus positional construction — you still get the composite form too:

data Vec2(x: int, y: int) // construct as Vec2(3, 4) or Vec2 { x: 3, y: 4 }

Sugar for “all fields let”, and it also emits [IsReadOnly] on the struct, telling the JIT to skip defensive copies on in parameters:

readonly data RegisterFile { rax: long, rbx: long, rip: long, flags: int }

Copy a value and overwrite specific fields, producing a new value — zero allocation, zero heap. with is value-only (using it on a ref data is an error):

let p1 = Point { x: 3, y: 4 }
let p2 = p1 with { x: 10 } // p1.x == 3, p2.x == 10, both y == 4

A bare type name as a field embeds it; the embedded type’s fields and methods are promoted — reachable directly on the outer type (t.x desugars to t.Vec2.x; the outer type’s own members shadow promoted names). Pointer embedding (*T) promotes through auto-deref:

data Transform { Vec2, var scale: double } // t.x, t.magnitude() reach into Vec2
data Entity { *Vec2, name: string } // promoted through the pointer (nullable)

Type parameters are reified — each instantiation is a real closed type at runtime, not an erasure:

data Pair<A, B> { first: A, second: B }
let p = Pair<int, string> { first: 1, second: "x" }

Browse data examples → · embedding

ref data — identity and the object world

Section titled “ref data — identity and the object world”

ref data is a CLR class: heap-allocated, reference equality, GC-tracked. Reach for it when you genuinely want identity, shared mutable state, framework interop, or constructors. Methods can live directly in the body, init(...) blocks are real constructors, and fields can carry defaults that run before the init body.

ref data Server {
let host: string = "localhost" // field default
var port: int = 8080
init(port: int) { self.port = port }
func describe() -> string = "{self.host}:{self.port}"
}

Inheritance is opt-in and sealed by default (open / abstract, virtual / abstract / : func, init(...) : base(...)) — it has its own page: Inheritance. Browse inheritance examples →

A choice is a sum type: a value that is exactly one of several named variants, each carrying its own payload. Emits as a tag enum + struct with factory methods (always a struct). It pairs with match.

choice AuthError {
invalidCredentials
accountLocked(untilUtc: DateTimeOffset) // multi-payload cases allowed
rateLimited(retryAfterMs: int)
}
let err = AuthError.invalidCredentials() // factory form

Generic choice is reifiedOption<int> is a real closed generic struct, not an erasure to object; a match binds the payload at its substituted type:

choice Option<T> { some(value: T), none }
let o = Option<int>.some(99) // typed Option<int>; .some(v) → v : int

Dot-case shorthand works when the type is known from context:

func findUser(id: Guid) -> Option<User> {
if user == nil { return .none }
return .some(user)
}

Browse choice examples →

The identity-carrying variant — an abstract base with a sealed subclass per case — for recursive, polymorphic structures (ASTs, UI trees, state machines).

ref choice Expr {
literal(value: int)
add(left: Expr, right: Expr)
neg(inner: Expr)
}

Two construction forms emit equivalent IL — the factory (mirrors the dot-shorthand) and the per-case subtype composite literal (the underlying CLR type per case is Outer_case):

let tree = Expr.add(Expr.literal(3), Expr.literal(4)) // factory
let sum = Expr_add { left: Expr_literal { value: 3 }, right: Expr_literal { value: 4 } } // composite

A match over a ref choice uses an isinst type pattern:

match expr {
.literal(v) { return v }
.add(l, r) { return eval(l) + eval(r) }
.neg(inner) { return 0 - eval(inner) }
}

A plain set of named constants with no payloads. Emits as a real CLR System.Enum (int32 underlying), interchangeable with C# enums. Variants without an explicit value start at 0 and increment; after an explicit value, auto-numbering resumes from value + 1:

enum Direction { north, south, east, west }
enum Level { a, b = 10, c } // a=0, b=10, c=11

Construct with the factory form — the trailing () is required, so the same call shape works across enum, choice, and ref choice:

let d = Direction.north()

A match on an enum needs a type hint, because variants flow through the dot-case shorthand:

match (d: Direction) {
.north { return "N" }
.south { return "S" }
default { return "?" }
}

Browse enum examples →

Interfaces emit as standard CLR interfaces. Conformance is nominal (data and ref data alike): a type implements an interface only when it names it after :. There’s no structural auto-satisfaction — methods still attach via promotion, but the type declares which interfaces it fulfills. The check is exact: every method matched by name, parameter types, and return type.

interface IDescribable { func describe() -> string }
data Client : IDescribable { name: string }
func describe(c: Client) -> string = "client: {c.name}"

Why nominal: the CLR is nominal underneath, declaring the interface makes the (value-type) boxing site explicit, and it lines up with how dependency registration is written. A type that would satisfy an interface it doesn’t declare gets a warning (ES2153) naming the : IFoo to add — a structural coincidence is never silently treated as conformance.

When only the pointer method set satisfies a declared interface (a pointer-receiver method, func f(x: *T)), the generated __Ptr_T wrapper implements the interface and the value type does not — the Go pointer-receiver case. See Pointers & memory.

T? is optional presence — distinct from Result<T, E>, which is for fallible operations.

T? of…Emits as
value type (int?, data?)Nullable<T>
reference type (string?, ref data?)unwrapped (already nullable) — holds as a generic arg too: Func<string, string?> is Func<string, string>

nil fills either (initobj Nullable<T> for value types, ldnull for reference types).

func find(id: int) -> User? {
if id == 0 { return nil }
return lookupUser(id)
}

[Name] and [Name(args)] pass straight through as CLR custom attributes — same syntax as C#, with constructor arguments resolved at bind time:

[Obsolete("use v2")]
[StructLayout(LayoutKind.Explicit)]
ref data Config { name: string }

([StackAlloc] / [HeapAlloc] are compiler directives, not CLR attributes — they pin a data’s form, then vanish from the assembly.)

derive emits real members at compile time (not runtime reflection). Place it above the type; combine directives with a comma:

derive equality, debug
data Packet { header: uint, length: int }
  • derive equality generates Equals(object), GetHashCode(), and == / !=.
  • derive debug generates ToString()Packet { header: 1, length: 2 }.
E#CLRSizeE#CLRSize
intInt324floatSingle4
uintUInt324doubleDouble8
longInt648boolBoolean1
ulongUInt648charChar2
shortInt162stringStringref
ushortUInt162voidVoid0
byte / sbyteByte / SByte1
  • Result<T, E> — error-as-value; ok(...) / error(...), ? propagation, combinators. See Errors.
  • Job / Job<T> — task handles from spawn / task func.
  • Chan<T> — typed channels.
  • TaskScope — structured concurrency.

The last three are covered in Async & concurrency. Type resolution order: primitives → built-in generics (Result, Chan) → current unit → other files → external .NET types.

Everything is internal by default; pub makes a declaration visible outside its assembly. Two levels only — there’s no private / protected. The module is the privacy boundary. Fields inherit the enclosing type’s visibility unless overridden with pub.

pub data Order { symbol: string, qty: int } // public type
data Internal { secret: int, pub name: string } // internal type, one public field

For the exact grammar and normative rules behind every type kind, see the Specification.