What is sicher?

sicher is an R package that brings type safety to R programming, similar to TypeScript for JavaScript. 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.

Key Features

  • Runtime Type Checking: Catch type errors during development
  • Intuitive Syntax: Use familiar operators like %:% and %<-%
  • Rich Type System: Support for primitives, lists, data frames, unions, and more
  • Typed Functions: Enforce parameter and return types on any function with typed_function()
  • Zero Runtime Overhead: Types are checked only during assignment
  • Gradual Adoption: Add types incrementally to existing code

Quick Start

Install the package and start using types immediately:

library(sicher)

# Basic type checking
x %:% Numeric %<-% 42
x <- "text"  # Error!
Error: Type error in 'x': Expected numeric, got string
Received: text

Why Type Safety in R?

R is a dynamically typed language, which offers great flexibility but can lead to:

  • Runtime errors from unexpected data types
  • Bugs that only appear with certain inputs
  • Maintenance difficulties in large codebases
  • API confusion when functions receive wrong types

sicher addresses these issues by providing:

  • Early error detection during development
  • Self-documenting code through type annotations
  • Better IDE support and code completion
  • Safer refactoring with type guarantees

Real-World Use Cases

Example 1:

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):

calculate_mean_payroll <- function(salaries) {
  average <- mean(salaries)
  return(average)
}

If the inputs have a correct data type, then we don’t have any issue:

calculate_mean_payroll(c(1800, 2300, 4000, 1222))
[1] 2330.5

Now, imagine that our input as following:

calculate_mean_payroll(c(1800, "2300", 4000, 1222))
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.

calculate_mean_payroll_safe <- function(salaries) {
  salaries %:% Numeric %<-% salaries
  average <- mean(salaries)
  return(average)
}
calculate_mean_payroll_safe(c(1800, "2300", 4000, 1222))
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:

calculate_mean_payroll_safe <- function(salaries) {
  salaries %:% Numeric %<-% salaries
  average <- mean(salaries)
  salaries <- "my salaries" # changing the type of the variable to string
  return(average)
}
calculate_mean_payroll_safe(c(1800, "2300", 4000, 1222))
Error: Type error in 'salaries': Expected numeric, got string of length 4
Received: [1800, 2300, 4000, 1222]

Example 2:

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:

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Using sicher you can, similar to the pydantic Python framework, define the expected structure of the response:

ToDo <- create_list_type(
  type_spec = list(
    userId = Numeric,
    id = Numeric,
    title = String,
    completed = Bool
  )
)

ToDo
<type: {userId: numeric, id: numeric, title: string, completed: bool} >

Then when fetching the data, you can safely check that the response corresponds to the expected object:

api_url <- "https://jsonplaceholder.typicode.com/todos/1"

response %:% ToDo %<-% httr::content(httr::GET(api_url))

response
$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:

invalid_response <- list(
  userId = 1, 
  id = 1, 
  title = "delectus aut autem", 
  completed = "yes" # Invalid! 
)

response <- invalid_response
Error: Type error in 'completed': Expected bool, got string
Received: yes
invalid_response <- list(
  userId = 1, 
  id = 1, 
  title = "delectus aut autem", 
  new_name_not_recognized = TRUE # Invalid! 
)

response <- invalid_response
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:

api_url <- "https://jsonplaceholder.typicode.com/todos/"

full_response <- httr::content(httr::GET(api_url))

length(full_response)
[1] 200
full_response[[1]]
$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:

ToDos <- ListOf(ToDo)

full_response %:% ToDos %<-% httr::content(httr::GET(api_url))

Now if we try to change the type of one element of the lists:

invalid_list <- full_response[[1]]$userId <- "A string" # Invalid type
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

Data Frame Schemas

# 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_df
Error: Type error in 'quantity': Expected integer, got double of length 2
Received: [10, 5]
sales_df <- data.frame(
  date = c("2024-01-01", "2024-01-02"),
  product = c("Widget A", "Widget B"),
  quantity = c(10L, 5L),
  price = c(29.99, 49.99),
  region = c("North", "South")
)

validated_sales %:% SalesData %<-% sales_df

Type System Overview

Built-in Types

  • Primitives: Integer, Double, Numeric, String, Bool
  • Containers: List, DataFrame, Function
  • Special: Any, Null

Type Modifiers

  • Scalar: Single element only
  • Readonly: Cannot be reassigned
  • Optional: Allows NULL values

Advanced Types

  • Unions: TypeA | TypeB
  • Size constraints: Numeric[3] (exactly 3 elements)
  • List types: Structured lists with field specifications
  • Data frame types: Column type specifications
  • Homogeneous lists: ListOf(ElementType)
  • Typed functions: typed_function(fn, params, .return)

Getting Started

Installation

Currently, install from Github with:

devtools::install_github("feddelegrand7/sicher")

Basic Usage

# 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

# 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"
)

Error Examples

See how sicher catches type errors:

# Wrong type assignment
x %:% Numeric %<-% "text"
Error: Type error in 'x': Expected numeric, got string
Received: text
# Missing required fields
User <- create_list_type(list(name = String, age = Numeric))
user %:% User %<-% list(name = "Alice")  # Missing age
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]
# Wrong data frame column types
Table <- create_dataframe_type(list(id = Integer, name = String))
df %:% Table %<-% data.frame(id = c("1", "2"), name = c("A", "B"))  # id should be integer
Error: Type error in 'id': Expected integer, got string of length 2
Received: [1, 2]