Types
Agency's type system is similar to TypeScript's. It includes user-defined generic type aliases and the built-in Record<K, V> / Array<T> / Schema<T> generics, but not all of TypeScript's more advanced features (conditional types, mapped types, etc.).
Agency types are cool because Agency generates Zod Schemas from the types automatically. This means you can use types to specify the structured output format for LLM calls.
let greet: number = llm("add 4 + 5")This will tell the LLM to respond with a number.
Here are some supported types:
Primitive types:
stringnumberbooleannullundefined(treated as null)regex(matches aRegExpliteral — note that LLMs can't return regex values through structured output, soregexcannot appear in anllm()return type)
Union types. Example:
let status: "success" | "error" = llm("Respond with either 'success' or 'error'")Array types. Example:
let items: string[] = llm("List 5 fruits")Object types. Example:
let user: {name: string, age: number} = llm("Provide a user object with name and age")You can define a new type:
type User = {
name: string;
age: number;
}You can describe a property on an object for the LLM:
type User = {
name: string # The name of the user
age: number # The age of the user
}Records
Use Record types when the set of keys isn't known up front, but you know generally what type all the keys and values need to be. Here is what a record type looks like.
// all keys are strings, all values are either "approve" or "reject"
let votes: Record<string, "approve" | "reject"> = {}
votes["alice"] = "approve"Note that in agency, because every type needs to be compilable to a schema, the keys can only be numbers, strings, or unions of numbers or strings (or type aliases that are numbers, strings, etc).
Another example:
type Status = "active" | "inactive"
let counts: Record<Status, number> = {
active: 5,
inactive: 2
}Note that if you use a union type for the keys, then all of the keys need to be defined in the object. For the example above, this will cause a type error:
// missing the 'inactive' key
let counts: Record<Status, number> = {
active: 5
}Generics
Types can take type parameters:
type Container<T> = { value: T }
type Pair<A, B> = { first: A, second: B }
let c: Container<number> = { value: 42 }
let p: Pair<string, number> = { first: "age", second: 30 }A type parameter can have a default. When all parameters have defaults, the alias can be used bare:
type StringMap<V = any> = Record<string, V>
let untyped: StringMap = {} // V defaults to any
let typed: StringMap<number> = { count: 1 } // V explicitDefault parameters must come after all required ones, mirroring TypeScript:
type Pair<A, B = string> = { first: A, second: B } // ok
type Pair<A = string, B> = { first: A, second: B } // errorRecursive generic aliases work:
type Tree<T> = { value: T, children: Tree<T>[] }Array<T> and Schema<T> are built-in generics.
Array<T> is equivalent to T[], and Schema<T> is equivalent to the type of schema(T). Schemas come later in the guide, but click here if you want to learn more.
Optional properties
Add ? after a property name to make it optional:
type Options = {
model?: string
temperature?: number
}This is shorthand for key: T | undefined — when the property is missing from a value, that's still type-correct. For example:
def configure(opts: Options): void { ... }
configure({ model: "gpt-4" }) // ok — temperature omitted
configure({}) // ok — both omittedExcess properties
When you write an object literal, every key in the literal must be in its declared type. This helps catch typos like modle: instead of model::
const cfg: Options = { modle: "gpt-4" } // error: Unknown property 'modle'This check fires only on object literals at the argument site. Assigning the same object to a variable first skips the excess property check, matching the behavior of TypeScript.
const badCfg = { modle: "gpt-4" }
const cfg: Options = badCfg // ok — excess property check doesn't apply hereSuppressing typecheck errors
How to opt out of typechecking:
// @tc-nocheck — silences every typecheck error in the file. Must appear at the top of the file:
// @tc-nocheck
// the rest of this file is not typechecked
def foo(x: number) {
print(x + "oops")
}// @tc-ignore — silences typecheck errors on the next line only. Must be on its own line, Agency does not allow trailing comments.
def take(x: number): void { print(x) }
node main() {
// @tc-ignore
take("not a number")
take(42)
}See also
- Schemas and validated types — the
T!shorthand forResult<T, string>, used for validation at runtime.