library(sicher)
# Basic type checking
x %:% Numeric %<-% 42
x <- "text" # Error!Error: Type error in 'x': Expected numeric, got string
Received: text
sicher (German for safe or certain and pronounced zeesher) is an R package that brings type safety to R programming, using a Typescript similar syntax. It allows you to define types for your variables and enforce them at runtime, catching type errors early and making your code more robust and maintainable.
sicher implements runtime type safety in R by attaching types to variables via active bindings rather than traditional attributes or classes. When a variable is declared with a type (e.g., x %:% Numeric %<-% 5), it is replaced by an active binding that wraps the value inside a closure with a private environment storing both the value and its associated type.
Every read and write to the variable is intercepted: reads return the stored value, while writes trigger validation through the type’s checker function before updating. This design provides strong guarantees that values cannot violate their declared types, enabling expressive and composable type definitions (including unions, structured lists, and data frame schemas).
%:% and %<-%extend()Enum(...)Literal(...)typed_function()Install the package and start using types immediately:
R is a dynamically typed language, which offers great flexibility but can lead to:
sicher addresses these issues by providing:
Suppose you write a function that calculates the average salary payout for employees. In a real company workflow, the data might come from, a CSV export from HR, manual editing in Excel, a database export and so on. Sometimes the numbers that are saved within those data sources can have invalid types (string for example):
If the inputs have a correct data type, then we don’t have any issue:
Now, imagine that our input as following:
Warning in mean.default(salaries): argument is not numeric or logical:
returning NA
[1] NA
Instead of getting an error, we get a warning and an NA value as a result. This is a very simple function but in a realistic example, you might have a script that uses thousands of lines of code and many functions to obtain a specific result, thus making this kind of debugging harder. We definitely expect more robustness from R.
Using sicher we can make sure that our data types remain valid and as we defined them.
Error: Type error in 'salaries': Expected numeric, got string of length 4
Received: [1800, 2300, 4000, 1222]
Note that when you define a type for a specific variable, that type is guarded throughout the execution of your code:
When working with Rest APIs, you want make sure that the data that you’re getting has a specific shape and type, consider the following API: https://jsonplaceholder.typicode.com/todos/1
It provides us with one object of the shape:
Using sicher you can, similar to the pydantic Python framework, define the expected structure of the response:
<type: {userId: numeric, id: numeric, title: string, completed: bool} >
When an API response grows over time, you can extend the shared base schema instead of duplicating it:
<type: {userId: numeric, id: numeric, title: string, completed: bool, createdAt: string, tags?: list<string> | null} >
Then when fetching the data, you can safely check that the response corresponds to the expected object:
$userId
[1] 1
$id
[1] 1
$title
[1] "delectus aut autem"
$completed
[1] FALSE
If the API for whatever reason started to produce invalid responses, sicher would catch it immediately:
Error: Type error in 'completed': Expected bool, got string
Received: yes
Error: Type error: Expected {userId: numeric, id: numeric, title: string, completed: bool}, got list
Details: Missing required field(s): completed (expected fields: userId, id, title, completed)
Received: list with fields: [userId, id, title, new_name_not_recognized]
The above URL returns only 1 object back, if we use the following URL https://jsonplaceholder.typicode.com/todos/ we get several (200) json objects back:
[1] 200
$userId
[1] 1
$id
[1] 1
$title
[1] "delectus aut autem"
$completed
[1] FALSE
Now how to set the type of a list of lists? using the ListOf function:
Now if we try to change the type of one element of the lists:
Error: Type error in 'full_response': Expected list<{userId: numeric, id: numeric, title: string, completed: bool}>, got list of length 200
Received: list of length 200
# Define data frame structure
SalesData <- create_dataframe_type(
col_spec = list(
date = String,
product = String,
quantity = Integer,
price = Numeric,
region = String
))
sales_df <- data.frame(
date = c("2024-01-01", "2024-01-02"),
product = c("Widget A", "Widget B"),
quantity = c(10, 5),
price = c(29.99, 49.99),
region = c("North", "South")
)
validated_sales %:% SalesData %<-% sales_dfError: Type error in 'quantity': Expected integer, got double of length 2
Received: [10, 5]
Integer, Double, Numeric, String, BoolList, DataFrame, FunctionAny, NullUse Enum(...) when a value must belong to a closed set:
Use Literal(...) when you want TypeScript-style exact scalar values:
TypeA | TypeBLiteral("left", "right") or Literal(200, 404)Enum("a", "b") or Enum(c("a", "b"))Numeric[3] (exactly 3 elements)ListOf(ElementType)typed_function(fn, params, .return)Currently, install from Github with:
# 1. Annotate variables with types
name %:% String %<-% "Alice"
age %:% Numeric %<-% 30
# 2. Create custom types
PositiveNumber <- create_type("positive number",
function(x) is.numeric(x) && all(x > 0))
value %:% PositiveNumber %<-% 42
# 3. Use type modifiers
single_value %:% Scalar(String) %<-% "hello"
constant %:% Readonly(Numeric) %<-% 3.14
maybe_value %:% Optional(String) %<-% NULL
# 3b. Restrict values with enums
status %:% Enum("draft", "published", "archived") %<-% "draft"
# 3c. TypeScript-style literal values
direction %:% Literal("left", "right") %<-% "left"
# 4. Create structured types
Person <- create_list_type(list(
name = String,
age = Numeric,
email = Optional(String)
))
person %:% Person %<-% list(
name = "Bob",
age = 25,
email = "bob@example.com"
)extend() composes new structured list types from an existing one. This is useful when several objects share a common core shape:
If a field in extra has the same name as one in the base type, the new definition wins and extend() warns so the override is not silent.
See how sicher catches type errors:
Error: Type error in 'x': Expected numeric, got string
Received: text
Error: Type error: Expected {name: string, age: numeric}, got list
Details: Missing required field(s): age (expected fields: name, age)
Received: list with fields: [name]
The behavior of sicher can be controlled globally using R’s options() mechanism. These options allow you to switch between strict type enforcement and a fully disabled mode depending on your use case.
sicher.modeControls how typed assignments (%:% and %<-%) behave.
"on" (default)"off"<-)You can configure the mode globally, for example, disabling strict type checking with:
Or enable strict typing with:
"on" (default) (strict mode) Use in situations where correctness and safety are important:
"off" (disabled mode) Use when you want to bypass the type system entirely:
Error: Type error in 'my_var_x': Expected numeric, got string
Received: string
It can also be temporarily scoped: