Skip to main content
A type guard names a rule about a value. When the guard passes, Forst remembers what the check proved. Later lines can use required fields without repeating the same validation. Use a guard for rules such as “this user is logged in” or “this request has input.” A failed ensure that uses the guard stops the function before the rest of the body runs.

Define a reusable check

This LoggedIn guard requires an AppContext that has both a session and a user.
The name in parentheses is the value you check (ctx). The type after it is the type the guard attaches to (AppContext). The name after the parentheses is the rule (LoggedIn). Use that name anywhere you can write an is check:
After this line, Forst knows that ctx.sessionId and ctx.user are present. A type guard records what a passing check proves about the value. A Bool return cannot do that. When LoggedIn() passes, later code can use the session and user as present.

Attach a guard to a named type

Attach a guard to a declared named type or struct shape. Define a named scalar type when you want guards on values such as passwords:
Strong then belongs to Password, not to every String in the program.

When to use which form

Use a guard inside if and ensure

Use if when both outcomes belong in the function. Use ensure with a guard inside a branch when that branch only makes sense if more checks pass. This handler ships a paid order. An unpaid order waits. A paid order must be unlocked and must have items:
Unlocked is order.locked is False(). HasItems is order.items is Min(1). If order.paid is false, fulfill returns "awaiting payment" and never runs those checks. If it is paid, a locked order returns OrderLocked and an empty cart returns EmptyCart. After both ensures pass, order.items[0] is safe. After the if, Forst does not treat the order as Unlocked or HasItems.

Keep guard bodies as checks

A guard body only checks the value in parentheses and any extra parameters. The same input must always produce the same result. You can use if, else if, else, is, and ensure. A guard cannot change values, use return, or read unrelated variables. If an if in a guard body does not match and has no else, the guard fails. A guard also cannot return a named error with else Unauthorized{}. Put the named error on the ensure that uses the guard:
Use or inside a guard when several conditions can satisfy the rule. See Accept several valid cases.

Add fields to a request shape

API procedures often share one context (who is calling) and differ on input (what they sent). You write that as one request object with a ctx field and an input field. AppContext and LoggedIn are the ones from the start of this page. Every procedure below already has ctx: AppContext. createTask adds a task name. getTask adds a task id. Callers pass ctx and input together.
type AppMutation = MutationArg.Context(AppContext) means every procedure already has ctx. .Input({ input: { name: String } }) adds this procedure’s payload. The call passes a session, a user, and a name. Inside createTask, ensure req.ctx is LoggedIn() reuses the LoggedIn guard. After that check, req.input.name is the string the caller sent, and req.ctx.sessionId is present. getTask is the same pattern with a different input. You do not repeat the context field on each procedure.

How Input and Context add fields

MutationArg is a request you can add fields to. In the example above, the Input and Context guards require a field to exist and give it a type. The names in parentheses (input, ctx) are those field types, not values at runtime. ensure req is { input } means the request must have an input field of the type passed into the guard. AppMutation.Input({ input: { name: String } }) is that guard applied to createTask. Request shapes that have no Forst name become hashed Go types in the Generated Go tab. Input and Context compile to helpers that return true; the type checker already required those fields. A failed LoggedIn check returns errors.New("ensure req.ctx is AppContext.LoggedIn(): want AppContext.LoggedIn()").

Caveats

Type guards are experimental. The examples above parse, type-check, and compile to Go. See the roadmap. A guard checks the value once at an ensure or if. Later changes do not run the guard again. A write to a field the check used drops the fact. See When a later write drops the fact for field writes, pointer fields, and concurrency. Narrowing after a check on a simple name (ensure ctx is LoggedIn()) is the reliable case. A check on a dotted path (ensure req.ctx is LoggedIn()) still has gaps. Editor hover may show the guard while later lines still see the original type. Narrowing inside an if branch may end with that branch. Code after the full chain can see the earlier type. A shape guard such as Input verifies that a field exists with the required shape. It never creates or fills that field. Generated Go uses functions or inline conditions for guard checks. An ensure that uses the guard follows the usual Go error return path. See Check a value for general narrowing limits.

Examples

See the type guard examples on GitHub: Continue with built-in constraints or apply guards with ensure.

Describe a shape

Built-in constraints on primitive fields.

Check a value

Using ensure … is … at call sites.