Skip to main content

Type Safety & Inference

Sluice's type system catches errors at compile time that would otherwise surface as silent wrong results or runtime crashes in production. Every field reference, operator argument, and pipeline output is statically validated against your schema.

How Types Flow Through Pipelines

Every pipeline stage is a function Agg<TIn, A> → Agg<TIn, B> — it takes a document shape and produces a new one. Sluice chains up to 15 stages, threading the output of each as the input to the next:

import { Schema as S } from "@effect/schema";
import { registry, $match, $group, $addFields, $unwind, $sort } from "sluice-orm";

const OrderSchema = S.Struct({
_id: S.String,
userId: S.String,
amount: S.Number,
items: S.Array(S.Struct({
name: S.String,
price: S.Number,
quantity: S.Number,
})),
status: S.Literal("pending", "paid", "shipped"),
});

const db = registry("8.0", { orders: OrderSchema });
const { orders } = db(client.db("shop"));

const result = await orders
.aggregate(
$match($ => ({ status: "paid" })),
// Type: still the original Order shape

$unwind("$items"),
// Type: { ... items: { name: string; price: number; quantity: number } }
// ↑ items is now a single object, not an array

$group($ => ({
_id: "$items.name",
revenue: $.sum($.multiply("$items.price", "$items.quantity")),
count: $.sum(1),
})),
// Type: { _id: string; revenue: number; count: number }
// ↑ completely new shape — previous fields are gone

$sort({ revenue: -1 }),
// Type: unchanged — $sort doesn't change the document shape
)
.toList();

// result: { _id: string; revenue: number; count: number }[]
// ↑ Fully inferred from the pipeline. No manual type annotation needed.

No generics were annotated. No as casts. The return type is computed from the pipeline definition.

Dot-Notation Path Inference

Sluice infers all valid dot-notation paths from your schema, including nested fields and array traversal:

const UserSchema = S.Struct({
_id: S.String,
name: S.String,
profile: S.Struct({
avatar: S.String,
settings: S.Struct({
theme: S.Literal("light", "dark"),
notifications: S.Boolean,
}),
}),
scores: S.Array(S.Number),
});

// Valid paths (autocomplete shows all of these):
// "name" → string
// "profile.avatar" → string
// "profile.settings.theme" → "light" | "dark"
// "profile.settings.notifications" → boolean
// "scores" → number[]
// "scores.0" → number

Type-Filtered Paths

Operators only accept paths that resolve to the correct type. $.avg only accepts numeric paths:

$group($ => ({
_id: "$department",
avgAge: $.avg("$age"), // ✅ age is number
// avgAge: $.avg("$name"), // ❌ Compile error: name is string
})),

The same applies to string operators ($.concat, $.toUpper), date operators ($.year, $.month), and array operators ($.size, $.arrayElemAt).

Positional Operators in Updates

Update operations support MongoDB's positional operators with full type safety:

await orders.updateOne(
() => ({ "items.name": "Widget" }),
{ $set: { "items.$.price": 29.99 } },
// ✅ items.$.price resolves to number — 29.99 is valid
);

The paths items.$, items.$[], and items.$[elem] all resolve through array elements correctly.

Compile-Time Error Messages

When something is wrong, Sluice produces structured error types that appear directly in your IDE:

Type Mismatch

$addFields($ => ({
doubled: $.multiply("$name", 2),
// ~~~~~~
// TypeError: $numeric expects number | numeric field ref, got string
}));

Invalid Field Reference

$match($ => ({ $nonExistent: 1 }));
// ~~~~~~~~~~~~~
// Error: "$nonExistent" is not a valid path in the schema

Bare Strings in Accumulators

A common MongoDB mistake — forgetting the $ prefix:

$group($ => ({
_id: "$department",
total: $.sum("score"),
// ~~~~~~~
// OpaqueError: Bare string "score" is not valid in accumulator position
// — did you mean "$score"?
}));

any Prevention

If a stage would produce any (e.g., from a loosely typed operation), Sluice catches it instead of letting it silently propagate:

// If a stage produces `any`, you get:
// OpaqueError: "Unexpected 'any' type detected — check your stage inputs"
// instead of the pipeline silently becoming untyped from that point on

Null Safety

Operators that can receive nullable inputs propagate nullability through the output type:

const UserSchema = S.Struct({
_id: S.String,
name: S.String,
score: S.optional(S.Number), // number | undefined
});

$addFields($ => ({
// $.ifNull narrows the type — removes null/undefined
safeScore: $.ifNull("$score", 0),
// Type: number (not number | undefined)

// $.cond preserves both branches
label: $.cond($.gt("$score", 50), "high", "low"),
// Type: "high" | "low"

// $.concat produces template literal types
greeting: $.concat("Hello, ", "$name"),
// Type: `Hello, ${string}`
}));

Context-Aware Variables

Array operators like $map, $filter, and $reduce introduce scoped variables with proper typing:

$addFields($ => ({
discountedPrices: $.map("$items", item =>
$.multiply(item("$price"), 0.9)
// ↑ item() resolves paths relative to the array element
// item("$price") → number
// item("$name") → string
),
// Type: number[]

expensiveItems: $.filter("$items", item =>
$.gt(item("$price"), 100)
),
// Type: { name: string; price: number; quantity: number }[]
}));

$reduce adds a $$value variable for the accumulator:

$addFields($ => ({
totalQuantity: $.reduce("$items", 0, ($value, $this) =>
$.add($value, $this("$quantity"))
),
// Type: number
}));

What This Means in Practice

The type system catches the most common MongoDB aggregation mistakes:

Common BugHow Sluice Catches It
Typo in field namePathType<T> — only valid paths autocomplete
Wrong operator argument typeNumericFieldRef<T> filters paths by resolved type
Forgot $ prefix on field refAccumulatorArgInput rejects bare strings
Used wrong accumulatorAccumulatorBuilder vs ExprBuilder context
Pipeline produces wrong shapeOutput type is computed, not annotated
any slips throughCheckedResult<T> catches and flags it
Null not handledNullishResult propagates nullability honestly

See the Advanced Typings page for complex real-world patterns including $facet, $lookup, union types, and discriminated unions.