How to use dplyr/tidyr from TypR
TypR does not require you to abandon the tidyverse. This guide shows how to use dplyr, tidyr, and related packages from typed code.
The core pattern: R {} blocks
Tidyverse functions rely heavily on non-standard evaluation (NSE), formulas,
and the pipe operator %>% / |>. These are fundamentally untyped constructs.
The idiomatic way to use them from TypR is R {} blocks:
let filtered <- R {
mtcars |>
dplyr::filter(cyl > 4) |>
dplyr::select(mpg, cyl, hp)
};
The R {} block captures its body verbatim and emits it as plain R code. It
is not type-checked, but it integrates perfectly with typed code that surrounds
it.
Declaring tidyverse signatures
For typed interop with dplyr, use @extern to declare the types of functions
you want to call with type safety:
@extern dplyr::filter: (data: Foreign<Any>, ...) -> Foreign<Any>;
@extern dplyr::mutate: (data: Foreign<Any>, ...) -> Foreign<Any>>;
@extern dplyr::select: (data: Foreign<Any>, ...) -> Foreign<Any>>;
@extern dplyr::summarise: (data: Foreign<Any>, ...) -> Foreign<Any>>;
@extern dplyr::arrange: (data: Foreign<Any>, ...) -> Foreign<any>;
With these declarations, dplyr::filter(df, .data$x > 1) is partially checked —
the compiler verifies the first argument is a dataframe, but ... is
intentionally untyped to allow NSE.
Mixing typed and untyped code
The best pattern is to do data preparation in R {} blocks and business
logic in typed functions:
@extern dplyr::filter: (data: Foreign<Any>, ...) -> Foreign<Any>;
@extern dplyr::mutate: (data: Foreign<Any>, ...) -> Foreign<Any>;
# Typed business logic
let compute_score <- fn(row: Foreign<Any>): num {
R { row$age * 0.3 + row$income * 0.7 }
};
# Untyped data preparation
let prepare <- fn(df: Foreign<Any>): Foreign<Any> {
R {
df |>
dplyr::filter(!is.na(age)) |>
dplyr::mutate(score = compute_score(.))
}
};
Using @importFrom
To generate proper roxygen2 import directives, use @importFrom:
@importFrom dplyr filter select mutate arrange summarise;
@importFrom tidyr pivot_longer pivot_wider;
@importFrom purrr map map_dbl;
This ensures your package's NAMESPACE file is correctly populated when the
package is built.
Working with dataframes in TypR
TypR supports dataframe types for typed access:
type PersonRow <- df[1]{ name: char, age: int };
let process <- fn(df: PersonRow): char {
df$name
};
However, for most tidyverse workflows, Foreign<Any> with R {} blocks is
more practical because tidyverse functions return generic tibbles, not
specific dataframe types.
Practical patterns
Typed wrapper around dplyr
@extern dplyr::group_by: (data: Foreign<Any>, ...) -> Foreign<Any>>;
@extern dplyr::summarise: (data: Foreign<Any>, ...) -> Foreign<Any>>;
let summarize_by_group <- fn(
df: Foreign<Any>,
group_col: char
): Foreign<Any> {
R {
df |>
dplyr::group_by(.data[[group_col]]) |>
dplyr::summarise(n = dplyr::n())
}
};
Type-safe column access
@extern dplyr::pull: (data: Foreign<Any>, var: char) -> Foreign<Any>;
let get_column <- fn(df: Foreign<Any>, col: char): Foreign<Any> {
dplyr::pull(df, col)
};
Best practices
-
Use
R {}for NSE — Tidyverse NSE (bare column names, formulas,...) cannot be type-checked. EmbraceR {}blocks for these operations. -
Signature the entry points — Add
@externfor dplyr/tidyr functions you call frequently. This gives you partial type safety on the first argument. -
Use
@importFrom— Generate proper namespace imports for your package. This is required for CRAN and good practice for any R package. -
Keep business logic typed — Do data transformation in
R {}blocks, but write the core logic of your package in typed TypR functions. -
Return
Foreign<Any>— Tidyverse functions return tibbles, which are opaque R objects. Do not try to type them as specific dataframe types unless you know the exact schema.
Where to go next
- Escape Hatches —
R {}blocks,extern,function() - Type existing R functions —
@signatures for plain functions - Compatibility with R — full interop walkthrough