Migrate an existing R package to TypR
This tutorial shows how to add TypR to an R package you already have, one file at a time. You do not need to rewrite anything — TypR works incrementally.
Duration: 20–30 minutes.
This is a tutorial. It teaches you by doing. For the full details, follow the links to the reference.
What you will learn
- How to add a
TypR/folder to an existing package - How to type one function at a time using
@signatures - How to convert R code to typed TypR and verify nothing broke
- How to mix typed and untyped code in the same package
Prerequisites
- An existing R package (or create a sample one below)
- R (≥ 4.1) and devtools
- The
typrcompiler — see the installation guide
Step 1: Create a sample package (skip if you have one)
If you do not have a package to migrate, create a minimal one:
devtools::create("mypkg")
Add a function in R/utils.R:
#' Normalize a numeric vector
#' @param x Numeric vector
#' @param center Logical, center the data
#' @param scale Logical, scale the data
#' @return Normalized numeric vector
#' @export
normalize <- function(x, center = TRUE, scale = TRUE) {
x <- scale(x, center = center, scale = scale)
as.vector(x)
}
#' Compute the mean of a vector
#' @param x Numeric vector
#' @return Numeric mean
#' @export
mean_val <- function(x) {
mean(x, na.rm = TRUE)
}
Run devtools::document() and devtools::test() to make sure everything works
before you start.
Step 2: Add the TypR folder
mkdir TypR
Create TypR/main.ty — the mandatory entry point:
# main.ty — typed code lives here
Run typr build to verify the toolchain works. The generated R/ files will
be mostly empty at this stage.
Step 3: Type one function with a signature
You do not have to rewrite the R code. A signature declares the types of an existing R function, giving you compile-time checking without touching the original code.
Add to TypR/main.ty:
@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.
Run typr build again. The signature is type-checked but the generated R code
still calls your original normalize function. Nothing changed at runtime.
Step 4: Add signatures for base R functions
As you write typed code, you will call base R functions. Declare their types so the compiler can check your usage:
@normalize: (x: [Any, num], center: bool, scale: bool) -> [Any, num];
@mean_val: (x: [Any, num]) -> num;
@as__character: (Self) -> char;
@as__numeric: (Self) -> num;
@paste0: (...Any) -> char;
@toupper: (char) -> char;
@nchar: (char) -> int;
The __ convention maps to . in R output: as__character becomes
as.character.
Step 5: Write typed code that calls the signed functions
Now add a typed function that uses the signatures:
@normalize: (x: [Any, num], center: bool, scale: bool) -> [Any, num];
@mean_val: (x: [Any, num]) -> num;
@as__numeric: (Self) -> num;
let standardize_and_report <- fn(x: [Any, num]): char {
let normalized <- normalize(x, true, true);
let avg <- mean_val(normalized);
paste("Mean after normalization:", as__character(avg))
};
The compiler checks that:
xis a numeric vectornormalizereceives the right typesmean_valreceives a numeric vectorpastereturns a character string
If you pass a string to normalize, it fails at compile time — not at
runtime.
Step 6: Convert R code to typed TypR (optional)
Once you are comfortable with signatures, you can convert R functions to full
TypR implementations. Move normalize from R/utils.R to TypR/main.ty:
@pub let normalize <- fn(x: [Any, num], center: bool, scale: bool): [Any, num] {
R {
x <- scale(x, center = center, scale = scale)
as.vector(x)
}
};
The R {} block preserves the original R logic. The function signature is
type-checked, but the body is emitted verbatim. This is the cleanest way to
migrate: typed signature + R body.
You do not have to convert everything at once. Keep functions in R/ and
add signatures in .ty files. Convert to full TypR only when you are ready.
Step 7: Test
Run your existing tests:
devtools::test()
Everything should pass. The generated R code is functionally identical to what you had before. TypR adds type checking on top — it does not change behavior.
Step 8: Add inline tests (optional)
As you convert more functions, add Test blocks next to them:
@pub let normalize <- fn(x: [Any, num], center: bool, scale: bool): [Any, num] {
R {
x <- scale(x, center = center, scale = scale)
as.vector(x)
}
};
Test {
test_that("normalize centers and scales", {
let x <- c(1.0, 2.0, 3.0, 4.0, 5.0);
let result <- normalize(x, true, true);
expect_equal(mean_val(result), 0.0, tolerance = 1e-10);
})
}
Step 9: Document
Run devtools::document() to regenerate man/ and NAMESPACE. The @pub
keyword generates @export directives, and type annotations become @param
and @return tags.
The incremental migration strategy
Here is the recommended approach for migrating any R package:
- Add
TypR/to your existing package - Sign the functions you call most — add
@signatures in.tyfiles - Write new code in TypR — use signatures for existing functions, write new logic in typed TypR
- Convert one function at a time — move from
R/toTypR/when ready - Run
devtools::test()after each step — verify nothing broke - Repeat until you are satisfied with the coverage
TypR never forces an all-or-nothing choice. You can stop at any point and your package works exactly as before.
Where to go next
- Create your first TypR package — start from scratch
- Model data with TypR types — records, unions, and dataframes
- How-To: Type existing R functions — the
@signature reference - Reference: Compatibility with R — the full interop walkthrough