add <- typed_function(
function(x, y) x + y,
params = list(x = Numeric, y = Numeric),
.return = Numeric
)
greet <- typed_function(
function(name, title = NULL) {
if (is.null(title)) paste("Hello,", name)
else paste("Hello,", title, name)
},
params = list(name = String, title = Optional(String))
)API Reference
Core Functions
typed_function(fn, params, .return)
Wraps a function with runtime type checks for its parameters and, optionally, its return value.
Parameters: - fn: The function to wrap. Its formals are preserved so callers use the same signature. - params: A named list mapping parameter names to their types (e.g. list(x = Numeric, y = String)). Only listed parameters are checked. Defaults to an empty list (no parameter checking). - .return: Optional return type. When NULL (the default), the return value is not checked. Accepts any sicher_type or sicher_union.
Returns: A function with S3 class "sicher_typed_function" that validates types on every call and delegates argument passing to fn unchanged.
Example:
create_type(name, checker)
Creates a new type with a name and validation function.
Parameters: - name: Character string for error messages - checker: Function that returns TRUE/FALSE for validation
Example:
Even <- create_type("even", function(x) is.numeric(x) && all(x %% 2 == 0))create_list_type(type_spec, extra = "error", alias = NULL)
Creates a type for lists with specific field requirements.
Parameters: - type_spec: Named list where names are field names and values are sicher_type or sicher_union objects - extra: What to do with fields that are not in type_spec: "error" (default) rejects them, "allow" keeps them unchecked, and "drop" removes them from the parsed value. - alias: Optional external names for every field: a naming convention ("camel", "pascal", "snake" or "kebab") or a function from field name to external name. Fields are read from their external name (or their R name) and stored under the R name. Fields wrapped in Alias() keep their own alias.
Returns: A sicher_type that validates list fields, rejects missing required fields, and handles unknown fields according to extra. The original field specification is stored internally so it can be reused by extend(), Pick(), Omit(), Partial(), Required() and KeyOf().
Example:
Person <- create_list_type(list(name = String, age = Numeric))
Loose <- create_list_type(list(name = String), extra = "allow")
check_type(list(name = "Ann", note = "kept"), Loose)[1] TRUE
Strict <- create_list_type(list(name = String), extra = "drop")
parse_as(list(name = "Ann", note = "removed"), Strict)$name
[1] "Ann"
Account <- create_list_type(list(account_id = Integer, created_at = String), alias = "camel")
parse_as(list(accountId = 1L, createdAt = "2024-01-05"), Account)$account_id
[1] 1
$created_at
[1] "2024-01-05"
extend(base, extra)
Creates a new structured type by extending an existing one created with create_list_type() or create_dataframe_type(). The result keeps the base type’s extra setting.
Parameters: - base: A sicher_type returned by create_list_type() or create_dataframe_type(). Primitive types and arbitrary create_type() validators cannot be extended because they do not carry a field specification. - extra: Named list of additional fields to merge into the base schema. Values must be sicher_type or sicher_union objects.
Returns: A new sicher_type created from the merged field specification. Fields declared in extra override fields with the same name in base; when that happens, extend() emits a warning.
Example:
Person <- create_list_type(list(
name = String,
age = Numeric
))
Employee <- extend(Person, list(
role = String,
department = Optional(String)
))
Manager <- extend(Employee, list(
reports = Numeric
))
Rows <- create_dataframe_type(list(id = Integer))
NamedRows <- extend(Rows, list(name = String))create_dataframe_type(col_spec, extra = "error", alias = NULL)
Creates a type for data frames with column specifications.
Parameters: - col_spec: Named list where names are column names and values are types - extra: What to do with columns that are not in col_spec: "error" (default), "allow" or "drop", as in create_list_type(). - alias: Optional external column names (a naming convention or a function), as in create_list_type(). Columns are renamed to their R names.
Example:
Table <- create_dataframe_type(list(id = Integer, name = String))infer_type(obj, strict = FALSE)
Infers a type from an example value. Factors, Date and POSIXct values are inferred as Factor(), Date and DateTime. With strict = TRUE, the observed lengths are also locked in with Scalar() and [n].
Example:
infer_type(list(name = "Ann", joined = as.Date("2024-01-01"), size = factor("M")))<type: {name: string, joined: date, size: factor<"M">} >
infer_type(c(1, 2, 3), strict = TRUE)<type: double[3] >
Enum(...)
Creates a type that only accepts values from a fixed set.
Parameters: - ...: Allowed scalar values, or a single atomic vector containing the allowed values
Example:
status %:% Enum("draft", "published", "archived") %<-% "draft"
priority %:% Enum(1, 2, 3) %<-% c(1, 2)
status <- "published"
status <- "Ho!"Error: Type error in 'status': Expected enum["draft", "published", "archived"], got string
Received: Ho!
priority <- c(3, 2, 1)
priority <- c(3, 2, 1, 100000)Error: Type error in 'priority': Expected enum[1, 2, 3], got double of length 4
Received: [3, 2, 1, 1e+05]
Enum(...) builds a regular sicher_type, so it works anywhere another type works:
status %:% Enum("draft", "published", "archived") %<-% "draft"
review <- typed_function(
function(state) paste("State:", state),
params = list(state = Enum("draft", "published", "archived")),
.return = String
)
review("published")[1] "State: published"
review("hola")Error: Type error in 'state': Expected enum["draft", "published", "archived"], got string
Received: hola
Literal(...)
Creates a type that only accepts exact scalar atomic literals.
Parameters: - ...: Allowed scalar atomic literal values
Example:
direction %:% Literal("left", "right") %<-% "left"
direction <- "right"
direction <- c("left", "right")Error: Type error in 'direction': Expected literal["left", "right"], got string of length 2
Received: [left, right]
status_code %:% Literal(200, 404) %<-% 200
status_code <- 404
status_code <- 200LError: Type error in 'status_code': Expected literal[200, 404], got integer
Received: 200
Literal(...) is stricter than Enum(...): it accepts only length-1 atomic values and compares them exactly, so 200 and 200L are different literals.
move <- typed_function(
function(direction) paste("Moving", direction),
params = list(direction = Literal("left", "right")),
.return = String
)
move("left")[1] "Moving left"
move("up")Error: Type error in 'direction': Expected literal["left", "right"], got string
Received: up
ListOf(element_type)
Creates a type for homogeneous lists.
Parameters: - element_type: Type that each list element must satisfy
Example:
Numbers <- ListOf(Numeric)Tuple(...)
A list with a fixed number of elements, each with its own type.
Point <- Tuple(Numeric, Numeric)Record(keys, values)
A named list (or named vector) with arbitrary keys. With one argument, keys default to String.
Scores <- Record(String, Numeric)
Prices <- Record(Numeric)DiscriminatedUnion(key, ...)
A union of list types selected by the value of the key field. Name each member with its discriminator value, or give each member a key = Literal(value) field.
Shape <- DiscriminatedUnion(
"kind",
circle = create_list_type(list(radius = Numeric)),
square = create_list_type(list(side = Numeric))
)Lazy(fn, name = "lazy")
Defers building a type until it is used, for recursive types.
Tree <- create_list_type(list(
value = Numeric,
children = ListOf(Lazy(function() Tree, name = "Tree"))
))FunctionOf(params, .return)
A function with the given parameters. Checked functions are wrapped with typed_function(), so later calls are checked too.
Formatter <- FunctionOf(list(x = Numeric), .return = String)Brand(type, brand) and brand(value, type)
Nominal types: brand() checks a value against the base type and adds the brand as a class.
UserId <- Brand(Scalar(String), "UserId")
uid <- brand("u-1", UserId)Factor(levels = NULL), MatrixOf(type, nrow, ncol), InstanceOf(class)
Factors (optionally with exact levels), matrices (optionally with an element type and dimensions) and objects inheriting from a class (S3, S4 or R6).
Size <- Factor(c("S", "M", "L"))
Grid <- MatrixOf(Numeric, nrow = 3, ncol = 3)
Model <- InstanceOf("lm")HasMethods(...)
Objects with methods for the given generics, whatever their class: S3 methods other than default (implicit classes such as numeric count), S4 methods, and function fields of R6 objects.
Model <- HasMethods("predict", "residuals")
is_type(lm(mpg ~ wt, data = mtcars), Model)[1] TRUE
match_type(type, ..., .default = NULL)
Builds a function that checks a value against type and calls the handler for its case: a DiscriminatedUnion() tag, an Enum()/Literal() value, a Factor(levels = ...) level, or a union member (named by its type name, e.g. integer). Missing handlers are an error unless .default is given; handlers for cases that do not exist are always an error.
describe <- match_type(Integer | String,
integer = function(x) "a number",
string = function(x) "some text"
)
describe(1L)[1] "a number"
Object Utilities
These work on types created with create_list_type() or create_dataframe_type() and keep their extra and alias settings.
| Function | Result |
|---|---|
Pick(type, ...) |
only the named fields |
Omit(type, ...) |
all fields except the named ones |
Partial(type) |
every field optional |
Required(type) |
every field required (Nullable() fields stay nullable) |
KeyOf(type) |
an Enum() of the field names |
Type Modifiers
Scalar(type)
Requires exactly one element (length 1). Also accepts union types.
Example:
SingleValue <- Scalar(Numeric)
Id <- Scalar(String | Numeric)Readonly(type)
Prevents reassignment after initial value.
Example:
Constant <- Readonly(String)Optional(type)
Allows NULL in addition to the base type.
Example:
MaybeString <- Optional(String)NonNA(type)
Rejects any value that contains at least one NA. The underlying type is validated first; NonNA adds an extra NA-free guarantee on top. Works with any sicher_type or sicher_union, including complex types produced by create_list_type().
NULL is not an NA — if the wrapped type already allows NULL (e.g. via Optional), NULL still passes NonNA.
Parameters: type — A sicher_type or sicher_union.
Example:
salary %:% NonNA(Numeric) %<-% c(1800, 2300, 4000)
salary <- c(1800, NA, 4000) # Error: value contains NA(s)Error: Type error in 'salary': expected non_na<numeric> (no NA values), but value contains NA(s) (at position 2)
# Compose with other modifiers
label %:% NonNA(Scalar(String)) %<-% "active"Between(min, max)
Creates a type that accepts numeric values within the closed interval [min, max]. All elements of a vector must satisfy the bounds; NA values are always rejected.
Parameters: - min — Single non-NA numeric scalar lower bound (inclusive). - max — Single non-NA numeric scalar upper bound (inclusive).
Accepts integer storage since is.numeric(1L) is TRUE in R.
Example:
age %:% Between(0, 150) %<-% 30
score %:% Between(0.0, 1.0) %<-% 0.95
score <- 1.5 # Error: 1.5 is outside [0, 1]Error: Type error in 'score': expected between[0, 1], but value [1.5] is outside [0, 1]
Matches(pattern)
Creates a type that accepts only character vectors whose every element matches the given Perl-compatible regular expression. The regex is compiled eagerly at construction time. An empty character(0) passes vacuously; pair with NonEmpty() to also require at least one element.
Parameters: pattern — A single non-NA character string (PCRE regex).
Example:
email %:% Matches("^[^@]+@[^@]+\\.[^@]+$") %<-% "user@example.com"
email <- "not-an-email" # Error: does not match patternError: Type error in 'email': expected matches("^[^@]+@[^@]+\.[^@]+$"), but value ["not-an-email"] does not match the pattern
# Combine with NonEmpty
tags %:% NonEmpty(Matches("^[a-z]+$")) %<-% c("foo", "bar")NonEmpty(type)
Creates a type that rejects zero-length values after validating the underlying type. For data frames, emptiness is defined as nrow == 0; for all other objects it is length == 0.
Parameters: type — A sicher_type or sicher_union.
Example:
tags %:% NonEmpty(String) %<-% c("r", "types")
items %:% NonEmpty(List) %<-% list(1, 2)
tags <- character(0) # Error: length > 0Error: Type error in 'tags': expected non_empty<string> (length > 0), but got an empty value
Nullable(type)
Allows NULL, but inside list and data frame types the field must be present (unlike Optional(), which lets the field be left out).
MaybeBio <- Nullable(String)MinLength(type, n), MaxLength(type, n)
Require at least / at most n elements (nrow() for data frames).
Top3 <- MaxLength(Numeric, 3)Unique(type)
Rejects duplicated elements (duplicated rows for data frames).
Ids <- Unique(Integer)Refine(type, predicate, message = NULL, name = NULL)
Checks type, then runs predicate on the value; message is used when it does not return TRUE.
Even <- Refine(Integer, function(x) all(x %% 2L == 0L), "must be even")Check(type, fn)
Checks type, then runs fn(value, ctx). The rule reports problems with ctx$issue(message, path = NULL), each at the field it names, so rules across fields point at the right input.
Range <- Check(
create_list_type(list(min = Numeric, max = Numeric)),
function(x, ctx) if (x$max < x$min) ctx$issue("must not be below min", path = "max")
)
safe_parse(list(min = 5, max = 1), Range)$issues$path[1] "max"
Transform(type, fn, output = NULL) and Pipe(...)
Transform() checks type, converts the value with fn and, if given, checks the result against output. Pipe() chains types, passing each parsed value to the next and stopping at the first that fails. JSON Schema describes the input side.
Trimmed <- Transform(String, trimws)
Share <- Pipe(Coerce(Numeric), Transform(Numeric, function(x) x / 100), Between(0, 1))
parse_as("42", Share)[1] 0.42
Default(type, value)
Replaces NULL with value. Inside list types, a missing field with a default is filled in.
Port <- Default(Numeric, 8080)Coerce(type, using = NULL)
Converts values before checking them (as.numeric(), as.integer(), as.character(), as.logical(), as.Date(), as.POSIXct(), factor(), or the using function). Conversions that would introduce NA are reported as errors.
NumberFromText <- Coerce(Numeric)Alias(type, alias)
Gives a list field or data frame column an external name. The value is read from alias (or from the R name when alias is absent) and stored under the R name; errors, dump_as(), to_json() and to_json_schema() use the alias. Outside list and data frame types it has no effect.
Customer <- create_list_type(list(customer_id = Alias(Scalar(Integer), "id")))
parse_as(list(id = 5L), Customer)$customer_id
[1] 5
Describe(type, description = NULL, title = NULL, examples = NULL)
Attaches a description, a title and examples to a type without changing how it checks values. to_json_schema() writes them as the description, title and examples keywords. examples is a list (or an atomic vector) of values that must each satisfy type.
City <- Describe(Scalar(String), "City name", examples = c("Paris", "Oslo"))
City<type: scalar<string> >
description: City name
Validation
check_type(value, type, context = NULL)
Returns TRUE or throws a sicher_type_error condition. Its issues element lists every problem with its path.
is_type(value, type)
Returns TRUE or FALSE, never throws for invalid data.
parse_as(value, type, context = NULL) and safe_parse(value, type, context = NULL)
parse_as() returns the parsed value (after Default(), Coerce(), extra = "drop" and FunctionOf() transforms) or throws. safe_parse() returns a list with success, data, issues (a data frame with path, message, expected, got, code) and error.
safe_parse(list(a = "x"), create_list_type(list(a = Numeric)))<parse result: 1 issue>
* a: Expected numeric, got string
validate_json(json, type, safe = FALSE, ...)
Parses JSON with jsonlite::fromJSON() and checks the result. Requires the jsonlite package.
to_json_schema(type, json = FALSE)
Converts a type to a JSON Schema (draft 2020-12) list, or a JSON string with json = TRUE. Properties are named by their aliases, and Describe() adds description, title and examples.
dump_as(value, type, by_alias = TRUE, exclude_null = FALSE)
The reverse of parse_as(). Checks value against type (throwing a sicher_type_error if it does not match), then renames list fields and data frame columns to their aliases, leaves out NULL fields when exclude_null = TRUE and removes brands. With by_alias = FALSE the R names are kept.
to_json(value, type, by_alias = TRUE, exclude_null = FALSE, pretty = FALSE, ...)
The reverse of validate_json(): dump_as() followed by jsonlite::toJSON(). Scalar() values become JSON scalars and other vectors become arrays, NULL and NA become null, and dates and date-times are written in ISO 8601 format. ... is passed to jsonlite::toJSON(). Requires the jsonlite package.
parse_env(type, prefix = "", env = Sys.getenv(), case_sensitive = FALSE)
Reads one environment variable per field of a list type and returns the settings as a named list. The variable for a field is prefix plus the field name in upper case (APP_ + db_url = APP_DB_URL), or the field’s Alias() without the prefix. Names match case-insensitively unless case_sensitive = TRUE. Values are converted to the field’s type (whole numbers, numbers, logicals such as yes/no/on/off/1/0, dates, date-times, JSON arrays and objects, or the Coerce() conversion). Unset and empty variables use the field’s Default() or are left out when Optional(); every problem is reported at once with the variable name. Refine() rules on the whole list type are checked too. Use env to pass variables explicitly, and readRenviron(".env") to load a .env file first.
field_errors(x) and problem_json(x, status = 400L, title = "Invalid request", type = "about:blank", instance = NULL)
Both take a safe_parse() result or a caught sicher_type_error. field_errors() returns a named list of messages per path, for showing errors next to form inputs. problem_json() returns an RFC 9457 “problem details” list (type, title, status, detail, errors) for HTTP error responses; each error has a path, a JSON pointer, a message and a code.
type_of(x) and untype(x)
type_of() returns the declared type of a typed variable (or NULL); untype() turns a typed variable back into a regular one.
Operators
%:%
Type annotation operator.
Usage:
variable %:% Type%<-%
Typed assignment operator.
Usage:
variable %:% Type %<-% value|
Union operator for combining types.
Example:
StringOrNumber <- (String | Numeric)&
Intersection operator. Two list (or data frame) types are merged into one with the fields of both; any other combination must satisfy both types.
Example:
Percent <- Numeric & Between(0, 100)[size]
Size constraint operator.
Example:
ThreeNumbers <- Numeric[3]Built-in Types
Primitive Types
Integer: Integer vectorsDouble: Double-precision numeric vectorsNumeric: Any numeric vectorsString: Character vectorsBool: Logical vectorsList: List objectsAny: Accepts any valueNull: Only NULL valuesFunction: Function objectsDataFrame: Data frame objectsDate:DatevectorsDateTime:POSIXctvectorsEnvironment: EnvironmentsPositive: Numeric values> 0(noNA)NonNegative: Numeric values>= 0(noNA)Whole: Numeric whole numbers, in integer or double storage
Error Handling
Type errors provide detailed context:
x %:% Numeric %<-% "text"Error: Type error in 'x': Expected numeric, got string
Received: text
Error messages include: - Expected type - Actual type - Location, such as todos[[2]]$title - Value preview (for debugging) - Positions of the failing elements of a vector, or rows of a data frame - A suggestion for misspelled field names, enum values and union tags
When several things are wrong, every problem is reported:
Point <- create_list_type(list(x = Numeric, y = Numeric))
pt %:% Point %<-% list(x = "a", y = "b")Error: 2 type errors found:
* Type error in 'pt$x': Expected numeric, got string
* Type error in 'pt$y': Expected numeric, got string
The condition has class sicher_type_error and carries the individual problems in e$issues:
err <- tryCatch(check_type("a", Numeric), sicher_type_error = function(e) e)
err$issues[[1]]$expected[1] "numeric"
Set options(sicher.mode = "warn") to report violations as warnings instead.
Global Options
The behavior of sicher can be controlled globally using R’s options() mechanism. The sicher.mode option accepts three values: "on" (default, violations are errors), "warn" (violations are warnings and the assigned value is kept) and "off" (no checking at all).
my_var_x %:% Numeric %<-% 10 # validated and bound with type enforcement
my_var_x <- "string" # trigger an errorError: Type error in 'my_var_x': Expected numeric, got string
Received: string
options(sicher.mode = "warn")
my_var_w %:% Numeric %<-% 10
my_var_w <- "string" # a warning, and the value is keptWarning: Type error in 'my_var_w': Expected numeric, got string
Received: string
my_var_w[1] "string"
Readonly() is still enforced in "warn" mode.
options(sicher.mode = "off")
my_var_y %:% Numeric %<-% 10
my_var_y <- "string" # nothing happensIt can also be temporarily scoped:
withr::with_options(
list(sicher.mode = "off"),
{
my_var_y %:% Numeric %<-% 10
my_var_y <- "string"
}
)