Types
Page 3 of 14 in Learn Jadpo
By the end you can give a todo's title rules, and see bad values refused while compiling and at the door.
On this page
On page 2 a todo's title was any Text, so the app took an empty title,
or one a megabyte long. On this page you give the title rules, add a
status that can only be "open" or "done", and an optional due date. The
compiler then refuses a bad value in your code before the app runs, and
the app refuses one in a request before your action sees it.
A type with rules
In TypeScript you might write type Title = string and check its length
with a validation library at the edge. In Jadpo the rules are part of
the type, and every value of the type follows them, wherever it came
from.
domains/:
/// What a todo says: at least one character, at most 200.type TodoTitle = Text { min_length: 1 max_length: 200}
TodoTitle is text with two rules: at least one character, and at most
200. You declare every type the same way: type, its name, =, the
type it builds on, and a block. Here the block holds rules. For the
other types on this page it holds fields or names instead. Text takes
rules on its length; numbers take min and max.
A type also says what a value means. A todo's title and a person's name
are both text, but they are different types, and Jadpo never turns one
into the other without being asked. So a function that wants a
TodoTitle can't be handed someone's name by mistake.
A value from a fixed list
A todo is either open or done. That's an enum: a type whose values are the names it lists.
domains/:
/// Whether a todo is still to do.type TodoStatus = Enum { open done}
You write a value as the type's name, a dot and the name:
TodoStatus.open. In JSON it is the name as a string, "open". Unlike a
TypeScript string union, a TodoStatus is never mixed up with plain
text.
A value that may be missing
Jadpo has no null. A type ending in ? may hold a value, or none,
and the compiler makes you deal with both. A todo's due date is
optional:
domains/, changed. title is now a TodoTitle, and due_at is new.
use todos/types { TodoTitle }/// What a caller sends to add a todo.type NewTodo = Input { title: TodoTitle due_at: Instant? { default: none }}
Instant is a built-in type for a moment in time. In JSON it is text in
the ISO 8601 form, such as "2026-11-02T09:00:00Z". Instant? is "an
instant, or none". default: none means a request may leave due_at
out, and then it is none. Without a default, a request would have to
send JSON null for due_at every time.
The response type gets the same fields, and the status in place of
done:
domains/, changed. done is replaced by status, title is a TodoTitle, and due_at is new.
use todos/types { TodoStatus, TodoTitle }/// A todo, as the app sends it back.type TodoView = Output { title: TodoTitle status: TodoStatus due_at: Instant?}
The other built-in types are Bool, Int for whole numbers, Decimal
for exact decimal numbers, Text, Bytes and Uuid. Beyond those,
Jadpo has about 250 standard types for common things, such as Email,
Url, Money and Duration, each with its rules already written. Use
one when it fits, rather than declaring your own.
A function
The action now has more to build, so give it a helper. A function is like an action, but it only works things out: it can't touch the database, the current time (page 5) or anything outside the app. So the same arguments always give the same result, and it's easy to test.
domains/, changed. Replace the whole file: the body of add_todo moves into the new function new_view.
use todos/types { NewTodo, TodoStatus, TodoView }/// A new todo, as the app sends it back: always open.function new_view(input: NewTodo) -> TodoView { return TodoView { title: input.title status: TodoStatus.open due_at: input.due_at }}/// Adds a todo. For now it only sends the todo back.action add_todo(input: NewTodo) -> TodoView { return new_view(input)}
A function that only one action uses can sit in that action's file, as
new_view does here. add_todo calls it by name.
Refused while compiling
Try breaking a rule, then put it back. In new_view, change
title: input.title to title: "", and check:
error: `""` isn't a valid `TodoView.title` --> domains/todos/actions/add_todo.jadpo:6:16 |6 | title: "" | ^^ breaks `min_length: 1` reason: `TodoTitle` declares `min_length: 1`, and the value has 0 characters. A literal is checked against every rule while compiling. next: Use a value that `TodoView.title` allows (agent) rule: type.invalid_literal help: https://jadpo.dev/docs/errors/type.invalid_literalcheck failed: 1 error, 9 files
A value written in the code is checked against the type's rules before
the app ever runs. A misspelt enum value is caught the same way. Change
TodoStatus.open to TodoStatus.finished:
error: `TodoStatus` has no variant `finished` --> domains/todos/actions/add_todo.jadpo:7:28 |7 | status: TodoStatus.finished | ^^^^^^^^ not a variant reason: `TodoStatus`'s variants are `open` and `done`. next: Use one of `TodoStatus`'s variants (agent) rule: type.unknown_enum_variant help: https://jadpo.dev/docs/errors/type.unknown_enum_variantcheck failed: 1 error, 9 files
The error calls open and done variants: that is Jadpo's word for the
values an enum lists.
Put both lines back as they were before you go on, or the next steps fail.
From here on, every time you change a route or a type a route uses,
jadpo check reports the API record as out of date, and the fix is
always jadpo api update. Later pages just say "update the record".
Update the API record
Check the project. The types changed what clients send and get back, so the API record is out of date:
error: `api/main.jadpo_record` is out of date --> api/main.jadpo_record:15:1 |15 | input NewTodo | ^^^^^^^^^^^^^ the source says otherwise reason: The record is a promise to API `main`'s clients, and the routes now say otherwise: changed `input NewTodo`, added `enum TodoStatus` and changed `output TodoView`. next: Run `jadpo api update`, and commit the record with the code (agent) rule: record.out_of_date help: https://jadpo.dev/docs/errors/record.out_of_date
Run jadpo api update. If your project is in git, git diff api/ shows
what changed, which is what a reviewer sees:
input NewTodo- title: Text+ due_at: Instant? default none+ title: Text { min_length: 1, max_length: 200 }++enum TodoStatus+ variant done+ variant open output TodoView- done: Bool- title: Text+ due_at: Instant?+ status: TodoStatus+ title: Text { min_length: 1, max_length: 200 }
jadpo check now says checked: 9 files.
Refused at the door
Run jadpo dev, and add a todo with a due date:
curl -i -X POST http://localhost:3000/todos \ -H 'content-type: application/json' \ -d '{"title": "Buy milk", "due_at": "2099-11-02T09:00:00Z"}'
HTTP/1.1 200 OKcontent-type: application/jsonx-request-id: req_f84f1aab5f40fb5acontent-length: 68date: Thu, 29 Oct 2026 21:54:15 GMT{"title":"Buy milk","status":"open","due_at":"2099-11-02T09:00:00Z"}
Now an empty title, and then a due date that isn't a date:
curl -X POST http://localhost:3000/todos \ -H 'content-type: application/json' \ -d '{"title": ""}'curl -X POST http://localhost:3000/todos \ -H 'content-type: application/json' \ -d '{"title": "Buy milk", "due_at": "next tuesday"}'
{"error":{"code":"invalid_input","message":"Request input is invalid.","request_id":"req_a7dafd2160674c97","details":{"problems":[{"field":"title","rule":"min_length"}]}}}{"error":{"code":"invalid_input","message":"Request input is invalid.","request_id":"req_d6c5b05a9b3540ad","details":{"problems":[{"field":"due_at","rule":"format"}]}}}
Each problem names the field and the rule it broke. format means the
text isn't in the form the type needs: here, not an ISO 8601 date. You
wrote the rule
once, on TodoTitle, and it holds in the code, in requests and in the
API description.
What you have
The same route, with a title that has rules, a status and an optional due date. Next, on page 4, the todos are stored, so they're still there after the request ends.
Read more in the reference: Named types, Enums and Values that may be missing.
Markdown for agents: /learn/learn-types.md.Built from Jadpo main at a707efb. Every file on this page is compiled and tested by Jadpo's CI, on the app as the pages before it built it.