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;
@externgenerates apackage::functioncall at runtime@importFromgenerates a roxygen2@importFromdirectiveForeign<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
- Identify the R function you want to type
- Determine the input types and return type
- Write a
@signature in a.tyfile - Run
typr build— the compiler checks your usage - Fix any type errors that surface
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
- Signatures, @extern & Foreign — full reference
- Escape Hatches —
extern,R {},function() - Use dplyr/tidyr from TypR — tidyverse interop patterns