Skip to main content
Type guards give a reusable name to a rule about your data. When a guard passes, Forst remembers what the check proved. Your code can then use required fields without repeating the same validation. In language design, a type guard is a predicate that narrows a base type to the values that satisfy it. This is the idea behind refinement types. Use a guard for rules such as “this request has input” or “this user is logged in.” A failed guard stops an ensure before the rest of the function runs.

Define a reusable check

This LoggedIn guard requires an AppContext with both a session and a user.
The declaration starts with is, followed by the value, its base type, and the guard name.
Use the guard anywhere you can use an is check.
After this line, Forst knows that ctx.sessionId and ctx.user are present.

Require fields on a shape

A shape guard describes fields that a value must contain. This is useful when several operations share a basic request shape and add their own requirements. In this example, MutationArg means a request passed to an API operation. Input requires an input field. Context requires a ctx field.

Build a more specific request type

Apply a guard to a type when every value of that type needs the same fields. AppMutation requires an AppContext in its ctx field.

Check the request in a handler

The handler adds an input requirement and checks that the request belongs to a logged in user.
The guard fails before task creation when the session or user is missing. After the check, the handler can safely read op.input.name.

Why a normal function is not enough

A function that returns Bool can report whether a value passed. Its return type does not explain what a true value proves about the input. A type guard records that relationship. When LoggedIn() passes, Forst knows which fields are present and lets later code use them with the refined type.

Current limits

Type guards are experimental. The examples above can be parsed, checked, and emitted as Go. Narrowing in more complex control flow is still being expanded. See the roadmap.

A check applies to the current value

A guard checks the value once at an ensure or if. Later changes do not run the guard again. Changing a field after the check can make the earlier result outdated.
Pointer fields such as sessionId: *String need particular care. Clearing the pointer does not undo the narrowing. Scoped immutability for ensure may address this later.
Narrowing reflects a single check at compile time. It does not lock the value at runtime.

Prefer simple variables

Narrowing works best when ensure checks a simple variable.
Paths such as ensure req.state is ... have partial support. Editor hover may show the guard while later code still sees the original field type.
Narrowing for compound ensure subjects is still being expanded. See the roadmap.

Keep guards free of side effects

A guard only checks its subject and parameters. The same input must always produce the same result. Inside a guard you can use if, else if, else, is, and ensure. A guard cannot change values, use return, add an or clause, or read unrelated variables.

Shape guards verify fields

A shape guard such as Input(input Shape) verifies that a field exists with the required shape. It never creates or fills that field. Changes to op.input or op.ctx after the guard are not tracked. Check the value again after changing it.

Narrowing after branches

Narrowing inside an if branch may end with that branch. Code after the full chain can see the earlier type. Branch merging will improve as union type support expands. See Ensure and narrowing for general narrowing limits.

Generated Go

Generated Go uses functions or inline conditions for guard checks. An ensure that uses the guard follows the normal Go error return path.

Examples

See the type guard examples on GitHub Continue with built in constraints or learn how to apply guards with ensure.

Shapes and constraints

Built-in constraints on primitive fields.

Ensure and narrowing

Using ensure … is … at call sites.