Inferring types

Infer<typeof schema> is the bridge between Sapphire’s runtime API and TypeScript’s static world. Every field carries two phantom brand types — _output (what parse returns) and _input (what parse accepts) — and Infer / InferInput are thin aliases over those. Modifiers like .optional(), .nullable(), and .default() shape both sides; this page explains how.

Minimal example

import { Sapphire, type Infer } from '@ascendance-hub/sapphire-core'

const a = new Sapphire()

const user = a.object({
  name: a.string(),
  age: a.number().optional(),
})

type User = Infer<typeof user>
// User = { name: string; age?: number | undefined }

Infer<> vs InferInput<>

TypeMeaning
Infer<typeof schema>The output of schema.parse(value).
InferInput<typeof schema>The value schema.parse(...) accepts.

For most fields these match — they differ only when a modifier creates an asymmetry. The clearest case is .default(value): at runtime, undefined is replaced by the default, so _input widens to T | undefined while _output stays T.

Modifier table

Modifier_input_output
a.string()stringstring
.optional()T | undefinedT | undefined
.nullable()T | nullT | null
.default('x')T | undefinedT
.optional().default('x')T | undefinedT | undefined

Note the last row: chaining .optional() before .default('x') keeps the output as T | undefined, because .optional() widens the brand types first and .default() doesn’t narrow them back. If you want the output strictly T, drop .optional() — .default(...) alone already makes the field optional at the input level.

Inside an object

a.object({...}) lifts optional-output children into ?: positions, so { name: string; age?: number | undefined } is what you get from Infer<> when age is a.number().optional(). The same lifting applies to _input via InferInput<>.

Pitfalls

[!WARNING] Don’t use ReturnType<typeof schema.parse>. TypeScript’s ReturnType reads the method’s declared return, but Sapphire’s Field<TOut, TIn> brand types are what carry the precise inference. Infer<typeof schema> walks straight to _output and preserves narrowing through chains.

[!WARNING] InferInput matters when piping user input through parse in a typed boundary — e.g. a tRPC procedure or an Express handler. .default(...) makes the input-side optional even when the output is required, so the caller can legally omit the field. The output of parse is always _output; only the function’s parameter type benefits from InferInput.