Skip to content

Types and Expr

This page is the reference for the type vocabulary and the Expr value handle. For the narrative version (and the staging-misuse failure modes), see values and types.

import type { EvsType, StringType, TupleType } from '@maxencerb/evs';
import type { Abi, AbiParameter } from 'viem';
declare const t: { readonly [k in Exclude<StringType, `${string}[]`>]: k } & {
array(elem: StringType | TupleType): StringType | TupleType;
struct(spec: Record<string, EvsType>): TupleType;
tuple(...items: readonly EvsType[]): TupleType;
fromOutputs(abi: Abi, name: string): EvsType; // derive a type from a function's outputs
fromAbiParameter(param: AbiParameter): EvsType; // derive a type from one ABI parameter
};

t is a frozen object whose every scalar property is its own literal type string — pure autocomplete sugar — plus three functions that build composite types. Raw type strings (and raw readonly AbiParameter[] arrays, anywhere a tuple type is expected) are accepted everywhere t.* is.

Members Value Notes
t.address 'address' 160-bit word
t.bool 'bool' canonical 0 or 1
t.uint8 through t.uint256 'uint8''uint256' one key per width in UintBits (every multiple of 8), 32 keys
t.int8 through t.int256 'int8''int256' same 32 widths, signed (two’s complement)
t.bytes1 through t.bytes32 'bytes1''bytes32' left-aligned fixed bytes, 32 keys
t.string 'string' dynamic, UTF-8
t.bytes 'bytes' dynamic byte string
t.array(elem) `${elem}[]` / TupleType function: a dynamic array of a scalar/array element (uint256[], string[], uint256[][]), or a tuple[] of a tuple element
t.struct(spec) TupleType (named components) function: a struct — Object.keys(spec) order is the encode order
t.tuple(...items) TupleType (positional) function: an unnamed positional tuple
t.fromOutputs(abi, name) EvsType (a TupleType for many/tuple outputs) function: derive a type from an as const ABI function’s outputs — one output → that output’s type, several → a named-component struct in ABI order (FromAbiOutputs); overloaded/unknown name throws (a non-const ABI degrades to EvsType)
t.fromAbiParameter(param) EvsType function: derive a type from one ABI parameter (AbiParamToEvsType) — a tuple… param → a TupleType, else the type string

t.array validates its element at the call site. t.struct/t.tuple build a TupleType descriptor — an abitype AbiParameter-shaped object viem infers directly.

import { t } from '@maxencerb/evs';
const fees = t.array(t.uint24); // 'uint24[]'
const Position = t.struct({ liquidity: t.uint128, owner: t.address }); // named struct → object
const Pair = t.tuple(t.address, t.uint256); // positional tuple → [address, bigint]
const amount = 'uint128'; // raw strings work identically
void [fees, Position, Pair, amount];

The full exported vocabulary, exactly as defined:

type Hex = `0x${string}`;
type UintBits =
| 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128
| 136 | 144 | 152 | 160 | 168 | 176 | 184 | 192 | 200 | 208 | 216 | 224 | 232 | 240
| 248 | 256;
type BytesSize =
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16
| 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32;
type UintType = `uint${UintBits}`;
type IntType = `int${UintBits}`;
type BytesNType = `bytes${BytesSize}`;
type WordType = UintType | IntType | 'address' | 'bool' | BytesNType;
type DynType = 'string' | 'bytes';
type ScalarType = WordType | DynType;
type ArrayType = `${ScalarType}[]` | `${ScalarType}[][]` | `${ScalarType}[][][]`;
type StringType = ScalarType | ArrayType; // every string-encoded type
interface NamedType {
readonly name: string; // '' for a positional tuple member
readonly type: string; // 'uint256', 'tuple', 'tuple[]', 'uint256[]', …
readonly components?: readonly NamedType[]; // present iff type starts with 'tuple'
}
interface TupleType {
readonly type: 'tuple' | 'tuple[]' | 'tuple[][]';
readonly components: readonly NamedType[];
}
type EvsType = WordType | DynType | ArrayType | TupleType;
type ArgType = EvsType; // script arg types — word, dynamic, string array, or a TupleType
type NumericType = UintType | IntType;
type BitsType = UintType | BytesNType;
Alias Meaning
Hex A 0x-prefixed hex string.
Address The address string type, re-exported from abitype (the same type viem uses).
UintBits The 32 legal integer widths: every multiple of 8 from 8 to 256.
BytesSize The legal bytesN sizes: 1 through 32.
UintType uint8uint256 as a template-literal union.
IntType int8int256.
BytesNType bytes1bytes32.
WordType Every type that fits one EVM word: unsigned and signed integers, address, bool, bytesN.
DynType The dynamic byte-string types: string and bytes.
ScalarType WordType or DynType — every non-array, non-tuple type.
ArrayType String-encoded arrays of a scalar element, up to three [] deep — uint256[], address[][].
StringType Every string-encoded type: a scalar or a string array.
NamedType One tuple/struct member — an abitype AbiParameter-shaped descriptor (recursive).
TupleType A struct/tuple type — { type, components }. Built by t.struct/t.tuple; see Composite types.
EvsType Every value type: word, dynamic, string array, or a TupleType.
ArgType The script-argument value type — an alias for EvsType (word, dynamic, string array, or TupleType); types ScriptIr.args and ArgSpec.type. Named args/params are narrower (string-encoded only) — that bound lives on namedArg(), not here.
NumericType The arithmetic domain: uintN or intN.
BitsType The bitwise/shift domain: uintN or bytesN.
LitOf<t> The host literal type accepted where a t is expected (table below).
IntoExpr<t> Expr of t, or a LitOf<t> literal.
Expr<t> The branded staged-value handle (below).

A TupleType is a struct or tuple type — an abitype AbiParameter-shaped descriptor that viem infers directly. Build one with t.struct (named members, inferred as an object) or t.tuple (positional members, inferred as an array); t.array lifts a tuple to a tuple[]. A raw readonly AbiParameter[] is accepted anywhere a TupleType is expected:

import { t } from '@maxencerb/evs';
import type { TupleType } from '@maxencerb/evs';
// t.struct → named components; runtime encode order is Object.keys insertion order.
const Position = t.struct({ liquidity: t.uint128, owner: t.address });
// ^? { type: 'tuple'; components: readonly [...] } (LitOf → { liquidity: bigint; owner: `0x${string}` })
// t.tuple → positional, unnamed components (LitOf → [bigint, `0x${string}`]).
const Pair = t.tuple(t.uint128, t.address);
// t.array of a tuple → a tuple[].
const positions = t.array(Position);
// nested struct: a member can itself be a TupleType.
const Outer = t.struct({ pos: Position, x: t.uint256 });
const types: readonly TupleType[] = [Position, Pair, positions, Outer];
void types;

LitOf<TupleType> delegates to abitype: an all-named tuple becomes an object, a positional tuple an array, recursing through nested components. TupleType flows everywhere a value type does — as a script arg (decoded into a Tuple handle), as a s.read argument or output, and as an s.return value. The handle surface — Tuple, Field, s.tuple — lives in the builder reference.

t.array(Position) lifts a struct to a tuple[] (a TupleType with type: 'tuple[]'). A composite-element array — tuple[], a one-level nested array (uint256[][]), or a dynamic-leaf array (string[]/bytes[]) — is an array of pointers in memory: the [length] prefix is followed by one pointer word per element, each pointing at the element’s own block (a tuple block, an inner array, or a bytes block). It shares the word-array handle surface — .length(), .at(i), s.newArray — and the same reference semantics; an s.read tuple[] output is an array handle whose .at(i) yields a typed Tuple element. The build/read/pass examples live in composite arrays. Still rejected at recording (UNSUPPORTED_V0): two-level tuple arrays (tuple[][]), arrays nested deeper than [][], and fixed-size T[N].

Several type-level helpers are exported for advanced inference: StructTypeOf, TupleTypeOf, TupleArrayOf, TypeToComponent, TupleLitOf, and TupleAsParam. You rarely name them — they power t.struct/t.tuple inference — but they are public for building your own type-level glue.

t.fromOutputs(abi, name) and t.fromAbiParameter(param) build a t.* type from existing ABI material instead of a hand-written t.struct/t.tuple — see deriving types from an ABI for the narrative and a runnable example. Their return types are exported:

  • FromAbiOutputs<abi, name> — the type of t.fromOutputs(abi, name): a single output’s type, or a TupleType struct over several outputs (named, in ABI declaration order). An unknown name or a non-const ABI widens to EvsType; an overloaded name throws at recording.
  • AbiParamToEvsType<p> — the type of t.fromAbiParameter(p): a TupleType for a tuple… parameter, else the scalar/array type string.

The component-level mirrors AbiParamToComponent and AbiParamsToComponents are exported too, for building your own ABI→type glue.

Every builder position typed IntoExpr accepts either an Expr or a plain host literal:

type LitOf<t extends EvsType> = t extends NumericType
? bigint | number
: t extends 'address'
? `0x${string}`
: t extends 'bool'
? boolean
: t extends BytesNType
? `0x${string}`
: t extends 'string'
? string
: t extends 'bytes'
? `0x${string}`
: t extends TupleType
? TupleLitOf<t> // delegated to abitype: object (all-named) or array (positional)
: t extends `${infer e extends StringType}[]`
? readonly LitOf<e>[]
: never;
type IntoExpr<t extends EvsType> = Expr<t> | LitOf<t>;

Literals are validated at recording time, with the call-site location (EvsTypeError on violation):

Literal Rule
number for uintN or intN must be a safe integer; range-checked against N
bigint for uintN or intN range-checked; negatives two’s-complemented for intN
boolean only for bool
0x string for address exactly 20 bytes; checksum NOT enforced (viem-permissive)
0x string for bytesN exactly N bytes
0x string for bytes any even-length hex
string for string UTF-8 encoded
JS array for T[] element-wise rules of T

Word literals canonicalize at recording; dynamic literals and literal arrays become bytecode data segments. All-literal pure operations fold at recording, and a fold that would certainly panic throws EvsTypeError (CERTAIN_PANIC) at that line instead. When inference needs help, construct the literal explicitly with s.lit (builder reference).

import type { EvsType } from '@maxencerb/evs';
declare const exprBrand: unique symbol;
interface Expr<t extends EvsType = EvsType> {
readonly [exprBrand]: t; // nominal, covariant phantom — you never construct an Expr yourself
readonly type: t; // runtime-readable type tag, e.g. 'uint256'
}

(Abridged — the methods are tabulated below.) An Expr is a branded handle to a recorded program value. The brand makes it nominal: an Expr<'uint8'> is not assignable where Expr<'uint16'> is expected. The only runtime-readable member is type.

Each method uses a this-parameter to restrict itself to the types it is defined on — calling add on an Expr<'address'> is a TypeScript error, not a runtime surprise. Every binary method also exists as a free function on the builder (s.add(a, b), s.lt(a, b), …) for literal-left operands; see the builder reference.

Available on numeric Exprs (this: Expr of t where t is a NumericType). All arithmetic is checked, matching solc 0.8 semantics — see arithmetic.

Method Signature Checked semantics
add add(rhs: IntoExpr<t>): Expr<t> Panic 0x11 on overflow
sub sub(rhs: IntoExpr<t>): Expr<t> Panic 0x11 on underflow
mul mul(rhs: IntoExpr<t>): Expr<t> Panic 0x11 on overflow
div div(rhs: IntoExpr<t>): Expr<t> Panic 0x12 on zero divisor; Panic 0x11 on signed minN / -1
mod mod(rhs: IntoExpr<t>): Expr<t> Panic 0x12 on zero divisor
Method Signature Notes
lt, gt, lte, gte lt(rhs: IntoExpr<t>): Expr<'bool'> Numeric types only; LT/GT vs SLT/SGT chosen from the static signedness
eq, neq eq(rhs: IntoExpr<t>): Expr<'bool'> Any word type; no string/bytes/array equality in v0
Method Signature Notes
and and(rhs: IntoExpr<'bool'>): Expr<'bool'> Eager, NOT short-circuiting — both sides always execute
or or(rhs: IntoExpr<'bool'>): Expr<'bool'> Eager, NOT short-circuiting
not not(): Expr<'bool'> Logical negation

For conditional execution use s.if (control flow).

Available on BitsType Exprs (uintN or bytesN). Results are re-canonicalized to the operand’s width; shifts never panic — bits leaving the lane are dropped.

Method Signature Notes
bitAnd, bitOr, bitXor bitAnd(rhs: IntoExpr<t>): Expr<t> Bitwise within the type’s lane
bitNot bitNot(): Expr<t> Complement, re-masked to the type’s width
shl, shr shl(bits: IntoExpr<'uint256'>): Expr<t> Shift amount is always uint256

Widening is free; narrowing is checked (Panic 0x11 on out-of-range values).

Method Signature Available on Semantics
toUint toUint(target: u): Expr<u> any numeric Expr checked against the target range
toInt toInt(target: i): Expr<i> any numeric Expr checked against the target range
asAddress asAddress(): Expr<'address'> Expr<'uint256'> or Expr<'bytes32'> checked: panics unless the top 96 bits are zero
asUint256 asUint256(): Expr<'uint256'> Expr<'bytes32'> free reinterpret
asBytes32 asBytes32(): Expr<'bytes32'> Expr<'uint256'> free reinterpret
import { evscript, t } from '@maxencerb/evs';
const casts = evscript({ name: 'casts', args: [t.bytes32] }, (s, raw) => {
const word = raw.asUint256(); // free reinterpret
const small = word.toUint(t.uint32); // checked narrowing — Panic 0x11 if out of range
const wide = small.toUint(t.uint256); // widening is free
const addr = raw.asAddress(); // checked: top 96 bits must be zero
return s.return({ small, wide, addr });
});
Method Signature Available on Semantics
length length(): Expr<'uint256'> string, bytes, any T[] byte length for string/bytes; element count for arrays
at at(i: IntoExpr<'uint256'>): Expr<elem> any T[] bounds-checked — Panic 0x32 on an out-of-range index
import { evscript, t } from '@maxencerb/evs';
const flags = evscript({ name: 'flags', args: [t.bytes4, t.int128] }, (s, mask, n) => {
const isNeg = n.lt(0n); // SLT — signedness comes from the static type
const top = mask.shr(16n); // re-masked to the bytes4 lane
const both = isNeg.and(mask.neq('0x00000000')); // eager bool logic
return s.return({ isNeg, top, both });
});