Types
Agency's type system is similar to TypeScript's.
Primitives
const name: string = "Alice"
const age: number = 30
const isActive: boolean = true
const nothing: null = null
const simpleRegex: regex = re/\d+/While JavaScript has two keywords that mean "empty", null and undefined, Agency just has one: null.
Arrays and objects
const names: string[] = ["Alice", "Bob", "Charlie"]
type Person = {
name: string
age: number
}
const person: Person = { name: "Alice", age: 30 }Union types
const status: "success" | "error" = "error"Type aliases
type User = {
name: string;
age: number;
}Optional properties
Add ? after a property name to make it optional:
type Options = {
// required
model: string
// optional
temperature?: number
}Records
// all keys are strings, all values are either "approve" or "reject"
const votes: Record<string, "approve" | "reject"> = {}
votes["alice"] = "approve"The keys can only be numbers, strings, or unions of numbers or strings. This is because in Agency, every type needs to be compilable to a schema. More on this later.
Another example:
type Status = "active" | "inactive"
let counts: Record<Status, number> = {
active: 5,
inactive: 2
}If you use a union type for the keys, all of the keys need to be defined. For example, 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 }
const c: Container<number> = { value: 42 }
const p: Pair<string, number> = { first: "age", second: 30 }Recursive generic aliases work:
type Tree<T> = { value: T, children: Tree<T>[] }Default type parameters
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>
// V defaults to `any`
const untyped: StringMap = {}
// V is explicitly set to `number`
const typed: StringMap<number> = { count: 1 }Default parameters must come after all required ones, mirroring TypeScript:
// ok
type Pair<A, B = string> = { first: A, second: B }
// error
type Pair<A = string, B> = { first: A, second: B }Built-in generics
Record<K, V>(see above)Array<T>(same asT[])Schema<T>(equivalent to the type ofschema(T))Result<S, E>(used for error handling, see theResulttype)
Schemas come later in the guide, but click here if you want to learn more.
Suppressing typecheck errors
// @tc-nocheck — Put it at the top of a file. Silences every typecheck error in 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.
def double(x: number) {
return x * 2
}
node main() {
// @tc-ignore
double("not a number")
}Excess property checks
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'Utility types
Agency ships five built-in utility types modeled on TypeScript, adapted to Agency optionality (optional means | null; there is no undefined):
| Type | What it does |
|---|---|
Partial<T> | Every property becomes nullable: p: V → p: V | null |
Required<T> | The inverse: strips null from every property |
Pick<T, K> | Keeps only the listed keys: Pick<User, "name" | "email"> |
Omit<T, K> | Removes the listed keys |
NonNullable<T> | Strips null from a single type: NonNullable<string | null> is string |
type User = {
name: string,
age?: number,
}
def updateUser(id: string, changes: Partial<User>): string {
// changes.name is string | null — guard before use:
if (changes.name != null) {
return changes.name // narrowed to string here
}
return "no name change"
}