> ## Documentation Index
> Fetch the complete documentation index at: https://forst-lang.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Type guards

> Reusable predicates that integrate with ensure and the type system.

**Type guards** define domain specific predicates that work with **`ensure`** and narrow types. They extend built in string, integer, and presence constraints.

## Declare a type guard

A guard is a pure predicate tied to a base type. **`LoggedIn`** requires a present session and user on **`AppContext`**:

<Tabs>
  <Tab title="Forst">
    ```ft theme={"languages":{"custom":["/languages/forst.json"]}}
    is (ctx AppContext) LoggedIn() {
    	ensure ctx.sessionId is Present()
    	ensure ctx.user is Present()
    }
    ```
  </Tab>

  <Tab title="Generated Go">
    ```go theme={"languages":{"custom":["/languages/forst.json"]}}
    // forst build
    func G_LoggedIn(ctx AppContext) error {
    	if ctx.SessionID == nil {
    		return /* Present() failure */
    	}
    	if ctx.User == nil {
    		return /* Present() failure */
    	}
    	return nil
    }
    ```
  </Tab>
</Tabs>

Syntax: **`is (subject BaseType) GuardName(…)`**, a pure predicate tied to a base type. Use it with **`ensure subject is GuardName()`**.

## Shape refinement guards

Guards can require extra fields on a shape, modeling cases like "this mutation must include input" or "this context must carry a session":

<Tabs>
  <Tab title="Forst">
    ```ft theme={"languages":{"custom":["/languages/forst.json"]}}
    type MutationArg = Shape

    is (m MutationArg) Input(input Shape) {
    	ensure m is { input }
    }

    is (m MutationArg) Context(ctx Shape) {
    	ensure m is { ctx }
    }
    ```
  </Tab>

  <Tab title="Generated Go">
    ```go theme={"languages":{"custom":["/languages/forst.json"]}}
    // forst build — illustrative; type-level guards do not emit Go helpers yet
    func G_Input(m MutationArg, input Shape) error {
    	// shape guard: m must include input field
    	return nil
    }

    func G_Context(m MutationArg, ctx Shape) error {
    	// shape guard: m must include ctx field
    	return nil
    }
    ```
  </Tab>
</Tabs>

Compose guards on types:

<Tabs>
  <Tab title="Forst">
    ```ft theme={"languages":{"custom":["/languages/forst.json"]}}
    type AppMutation = MutationArg.Context(AppContext)
    ```
  </Tab>

  <Tab title="Generated Go">
    ```go theme={"languages":{"custom":["/languages/forst.json"]}}
    // forst build
    type AppMutation = MutationArg // Context(AppContext) encoded on the type
    ```
  </Tab>
</Tabs>

Then enforce them in handlers:

<Tabs>
  <Tab title="Forst">
    ```ft theme={"languages":{"custom":["/languages/forst.json"]}}
    func createTask(op AppMutation.Input({
    	input: { name: String }
    })): Result(String, Error) {
    	ensure op.ctx is LoggedIn()
    	return op.input.name
    }
    ```
  </Tab>

  <Tab title="Generated Go">
    ```go theme={"languages":{"custom":["/languages/forst.json"]}}
    // forst build
    func createTask(op AppMutationInput) (string, error) {
    	if err := G_LoggedIn(op.Ctx); err != nil {
    		return "", err
    	}
    	return op.Input.Name, nil
    }
    ```
  </Tab>
</Tabs>

Unauthorized calls fail at the guard, before task creation logic runs.

## Why guards instead of free functions?

A standalone `func isLoggedIn(ctx AppContext) bool` validates at runtime but does **not narrow** `ctx` for the typechecker. Type guards are first-class: the compiler records the refined type after a successful **`ensure`**.

## Caveats

Type guards are **experimental**. Parsing, typechecking, and Go emission work for the patterns above. Narrowing across all control flow positions is still being expanded. See the [roadmap](/docs/resources/roadmap).

In the examples above, **`MutationArg`** and **`AppMutation`** refer to API **mutation request** shapes, like a GraphQL or tRPC mutation argument. They have nothing to do with changing values after a guard passes.

### Checks run once

A guard runs once at the **`ensure`** or **`if`** site. After it passes, the compiler remembers the narrowed type for later lines. It does **not** watch the value for changes.

If you reassign the variable or mutate its fields afterward, runtime state can fall out of sync with what the compiler still believes:

```ft theme={"languages":{"custom":["/languages/forst.json"]}}
ensure ctx is LoggedIn()
ctx.sessionId = nil   // runtime: no longer logged in
// compiler may still treat ctx as LoggedIn here
```

Pointer fields (like `sessionId: *String`) are especially risky. Clearing or replacing the pointer does not undo the narrowing. Planned **`ensure`** scoped immutability may address this later. It is not available today.

<Warning>
  Narrowing reflects a single check at compile time. It does not lock the value at runtime.
</Warning>

### Narrowing on simple names

Successor narrowing after **`ensure`** works best on **simple identifiers** (`ensure x is …`, then use `x`).

Compound paths like **`ensure req.state is …`** have partial support. Hover may show the guard predicate, but the typechecker does not fully re-register a refined type on the path. The examples use **`ensure op.ctx is LoggedIn()`** because **`op`** is the simple binding. Do not assume every dotted path behaves the same.

<Info>
  Full narrowing on compound **`ensure`** subjects is still being expanded. See the [roadmap](/docs/resources/roadmap).
</Info>

### Guards are pure predicates

Inside a guard body you cannot modify the receiver or its parameters. Guards must be deterministic and side effect free. Only **`if`**, **`else if`**, and **`else`** branches with **`is`** assertions and **`ensure`** refinements are allowed. No **`return`**, no **`or`** clauses inside guard **`ensure`**, and no references to variables outside the receiver and guard parameters.

### Shape guards work at the type level

Shape guards like **`Input(input Shape)`** add a required field to the **type**. They validate that the value has that structure at runtime. They do not insert data. **`ensure m is { input }`** checks presence and shape. It does not populate **`input`**.

Even when a shape guard passes, mutating **`op.input`** or **`op.ctx`** afterward is not tracked. See the checks run once caveat above.

### Control flow limits

Narrowing inside an **`if`** branch does not always carry past the end of the chain. The continuation may see the pre-**`if`** type until union types exist. Branch merge after **`ensure`** successors is also still expanding.

See [Ensure and narrowing § Caveats](/docs/language/ensure-and-narrowing#caveats) for general narrowing behavior.

## What gets emitted

Guards transpile to Go functions or inline checks used by **`ensure`** lowering, with structured errors where you supply **`or`** clauses.

## Try it

From the Forst repository:

```bash theme={"languages":{"custom":["/languages/forst.json"]}}
task example:shape-guard
task example:basic-guard
```

Sources: [`examples/in/rfc/guard/`](https://github.com/forst-lang/forst/tree/main/examples/in/rfc/guard/)

## Related

<CardGroup cols={2}>
  <Card title="Shapes and constraints" icon="table-columns" href="/docs/language/shapes-and-constraints">
    Built-in constraints on primitive fields.
  </Card>

  <Card title="Ensure and narrowing" icon="filter" href="/docs/language/ensure-and-narrowing">
    Using `ensure … is …` at call sites.
  </Card>
</CardGroup>
