Skip to main content

How to type existing R functions with @ signatures

This guide shows how to add type safety to R functions you already have — base R, your own helpers, or third-party packages — without rewriting them.

Why use signatures?

By default, untyped R functions accept Any and return Empty. The compiler cannot check their usage, so mistakes slip through to runtime. A signature declares the types an R function expects and returns, giving you compile-time checking without touching the original R code.

Basic signature

Suppose you have an R function in R/utils.R:

normalize <- function(x, center = TRUE, scale = TRUE) {
x <- scale(x, center = center, scale = scale)
as.vector(x)
}

Declare its type in a .ty file:

@normalize: (x: [Any, num], center: bool, scale: bool) -> [Any, num];

The @ prefix tells TypR this is a signature for an existing R function — no body needed. The compiler will now check every call to normalize against these types.

Working with base R

Base R functions benefit immediately from signatures. The __ convention maps to . in R output:

@toupper: (char) -> char;
@nchar: (char) -> int;
@paste0: (...Any) -> char;
@as__character: (Self) -> char;
@as__numeric: (Self) -> num;

Now toupper("Hi") is type-checked as char -> char, and toupper(7) would fail at compile time instead of producing a silent coercion at runtime.

Signatures with overloading

Some R functions accept multiple input types. Repeat the signature with different type parameters:

@abs: (int) -> int;
@abs: (num) -> num;
@sqrt: (int) -> num;
@sqrt: (num) -> num;

The compiler picks the right overload based on the argument type.

Using @extern for external packages

When calling functions from other packages, use @extern to declare both the package and the type:

@extern stats::sd: (x: [Any, num]) -> num;
@extern stats::lm: (formula: char, data: Foreign<Any>) -> Foreign<Any>;
@extern base::readRDS: (path: char) -> Foreign<Any>;
@importFrom dplyr filter select mutate;
  • @extern generates a package::function call at runtime
  • @importFrom generates a roxygen2 @importFrom directive
  • Foreign<Any> wraps opaque R values that pass through TypR untouched

Practical example: typing a dplyr pipe

Say you call dplyr::filter on a dataframe. You can declare its signature and use it in typed code:

@extern dplyr::filter: (data: Foreign<Any>, ...) -> Foreign<Any>;

let filter_adults <- fn(df: Foreign<Any>): Foreign<Any> {
df |> filter(.data$age >= 18)
};

Step-by-step workflow

  1. Identify the R function you want to type
  2. Determine the input types and return type
  3. Write a @ signature in a .ty file
  4. Run typr build — the compiler checks your usage
  5. Fix any type errors that surface
tip

Start with the functions you call most often. You do not have to sign everything at once — add signatures incrementally as you find value in the type checking.

Where to go next