Every runtime value in a script has an evs type — a Solidity-style type string like 'uint256', 'address', or 'uint256[]'. This page covers the type vocabulary, the Expr handles that represent runtime values, which plain JS literals you can pass where, and what happens when you accidentally treat a handle like a real value.
uint8…uint256 and int8…int256 (multiples of 8), address, bool, bytes1…bytes32
one canonical EVM word
Dynamic types
string, bytes
memref — a pointer to [length][payload]
Array types
T[] where T is a word type, a dynamic type, a one-level array (T[][]), or a struct/tuple (tuple[])
memref — [length] then either one word per element (word element, inline) or one pointer per element (composite element)
Composite types
structs and tuples (t.struct/t.tuple)
memref — a pointer to a packed block, one word per member
The exported type aliases mirror this taxonomy: EvsType is the whole set; WordType, DynType, ArrayType, and TupleType are the kinds; and two cross-cutting unions matter for operations — NumericType (uintN and intN) and BitsType (uintN and bytesN). The full alias table is in the types reference.
A composite-element array — tuple[], a string array (string[]/bytes[]), or a one-level nested array (uint256[][]) — is an array of pointers: the [length] prefix is followed by one pointer word per element, each pointing at that element’s own block (a tuple block for tuple[], an inner array for T[][], a bytes block for string[]). This is exactly Solidity’s Struct[]/T[][]/string[] memory layout. You read and build it through the same array handle as a word array (.at(i), .length(), s.newArray), and it carries the same reference semantics — see composite arrays below.
Still rejected at recording (UNSUPPORTED_V0): two-level tuple arrays (tuple[][]), arrays nested deeper than [][], and fixed-size arrays T[N] (not modeled).
t is autocomplete sugar: every scalar property is its own name as a literal string (t.uint256 is exactly 'uint256'), t.array(elem) builds an array type, and t.struct/t.tuple build composite types. Raw type strings are accepted everywhere t.* is:
An Expr<t> is a phantom-typed handle to a value that will exist at run time. It is not the value: you cannot read a number out of it, and you cannot construct one yourself. Handles are produced by the script’s positional args, s.lit, s.read, s.env, cell reads, and every operation — and consumed by other operations, sub-call parameters, and s.return.
The brand is nominal (a unique symbol), so nothing structurally fakes an Expr, and each handle carries a runtime-readable .type tag. Word-typed handles stand for single words; dynamic and array handles are memrefs — references to a memory buffer, with reference semantics where mutation is possible (see MutArray in control flow).
const first=xs.at(0n);// Expr<'uint128'> — bounds-checked, Panic(0x32) if out of range
returns.return({ n, first });
});
The arithmetic, comparison, bitwise, and conversion methods on Expr are covered in arithmetic; the exhaustive method tables are in the types reference.
Solidity structs and tuples are first-class. Build a composite type with t.struct (named members — inferred as an object) or t.tuple (positional members — inferred as an array); both produce a TupleType descriptor that viem infers directly. A composite value is a memref to a packed block of one word per member (a static member inline, a dynamic/nested member as a pointer), and its handle is a Tuple — not an Expr — exposing a Field per member.
Three things you do with composites:
Decode a struct return. A 'tuple' output of s.read becomes a Tuple handle; read members by name with .field.get():
position: pos, // a Tuple handle is returnable directly — the whole struct flows out as an object
});
},
);
A Tuple handle is returnable directly — s.return({ position: pos }) hands the whole struct back, abitype-typed. pos.expr() does the same thing and is still valid when you want the bare memref Expr (to pass on as a call argument, say).
Construct a tuple with s.tuple. Allocate a struct/tuple in memory and fill members — by an init record, or by Field.set afterwards:
A TupleType arg works the same way: declare it on the header (args: [QuoteParams]) and the body receives a Tuple handle instead of an Expr. Reference semantics apply throughout — a Tuple handle is a pointer, so passing it (or aliasing it) shares the same block and a later .set() is visible everywhere. The full Tuple/Field/s.tuple surface is in the builder reference.
When a type already exists in an ABI, you do not have to retype it. t.fromOutputs(abi, name)
derives an evs type straight from a function’s ABI outputs, and t.fromAbiParameter(param) does
the same for a single ABI parameter:
t.fromOutputs(abi, name) — a function with one output returns that output’s type (a
TupleType for a tuple output, else the scalar
or array type string); a function with several outputs returns a TupleType struct over them,
with components in ABI declaration order.
t.fromAbiParameter(param) — one ABI parameter → its evs type (a tuple… param becomes a
TupleType, anything else its type string).
A derived type flows everywhere a hand-written t.struct/t.tuple does — as a script arg, a
s.read argument or output, an s.newArray element, or an s.return value. Because the
components come from the ABI in declaration order, a t.fromOutputs(abi, name) type is
interchangeable with a t.struct written in the same order and with a
s.read({ …, struct: true }) decode of
that same function. An overloaded or unknown function name throws EvsTypeError at the call site
(a non-as const ABI degrades to EvsType rather than erroring, mirroring s.read). The exact
return types (FromAbiOutputs, AbiParamToEvsType) are in the
types reference.
Arrays whose element is itself composite — tuple[] (arrays of structs), one-level nested arrays (uint256[][]), and dynamic-leaf arrays (string[], bytes[]) — are first-class. They share the word-array handle surface (.length(), .at(i), s.newArray); the only difference is in memory each slot holds a pointer to the element’s own block instead of an inline value (an array-of-pointers, exactly Solidity Struct[]/T[][]/string[] memory). Reference semantics carry over: an array handle is a pointer, so the element handles you read from it alias the array’s slots.
Read a tuple[] output. A 'tuple[]' output of s.read is an array handle; .at(i) returns the i-th element as a typed Tuple — read its fields by name:
A tuple[] output unwraps to readonly Struct[] at the viem call site; uint256[][] → readonly (readonly bigint[])[]; string[] → readonly string[].
Construct a tuple[] with s.newArray.s.newArray admits composite elements (a struct/tuple, a one-level T[], string/bytes). arr.get(i) returns the element handle (a Tuple for a struct element) — a reference into the array’s slot, so filling its fields writes through:
A composite-array literal (a plain JS array of struct objects, of inner arrays, or of
strings) is accepted wherever a value type is coerced — a call argument, an s.return value, or
a tuple/array member slot (s.tuple / Field.set / MutArray.set) — and is built at record time
into a fresh [length][p0…] block with reference semantics (not a flat data segment). The
string-typed shapes (uint256[][], string[]) can also be materialized directly with
s.lit, because their type is a plain string; a tuple[] literal
cannot — s.lit only accepts string-encoded types and rejects the t.array(t.struct(...)) object
with EvsTypeError (TYPE_MISMATCH), so build a tuple[] with s.newArray (above) or pass it
through a coerced position (a call arg, below):
Most operand positions are typed IntoExpr<t> — either an Expr<t> or a plain JS literal of the matching shape (LitOf<t>). Literals are validated at recording time, with the call site’s location attached to any EvsTypeError:
evs type
Accepted JS literal
Validation rule
uintN / intN
bigint or number
numbers must be safe integers; range-checked against N; negative bigints two’s-complemented for intN
bool
boolean
—
address
0x string
exactly 20 bytes; checksum not enforced (viem-permissive)
bytesN
0x string
exactly N bytes
bytes
0x string
any even-length hex
string
string
UTF-8 encoded
T[]
readonly array of T literals
element-wise rules of T
t.struct/t.tuple
object (named) or array (positional) of member literals
member-wise rules; omitted members default to zero
Word literals canonicalize at recording. Dynamic literals and word-array literals become bytecode data segments, materialized by CODECOPY the first time the script uses them. A composite-array literal (a tuple[]/T[][]/string[] JS array) is instead built at record time into a fresh array-of-pointers block (see composite arrays), so it has reference semantics like a constructed array. A T[][]/string[] literal can also be materialized directly with s.lit (its type is a plain string), but a tuple[] literal is built only when it flows through a coerced value position (a call arg, an s.return slot, or a member) — s.lit requires a string type and rejects the t.array(t.struct(...)) object.
s.lit(type, value) is the explicit constructor for when inference has no expected type to coerce against — a standalone constant, or the first operand of a free function.
Free functions need at least one Expr operand.s.add(1n, 2n) has no type to infer the operation from, so it throws EvsTypeError at recording with the fix in the message: type one operand with s.lit.
Out-of-range literals are recording-time errors, not runtime reverts: s.lit(t.uint8, 300) throws EvsTypeError immediately.
Because handles are plain objects at run time of your program, JS will happily let you pass them places they make no sense. evs traps the common cases: every handle installs throwing valueOf, toString, toJSON, and Symbol.toPrimitive, so arithmetic, template strings, loose equality, and JSON.stringify all throw EvsStagingError at the offending line — citing both the misuse site and where the handle was recorded.
The following does not compile (and the parts TS cannot reject throw at recording time):