Skip to main content

How to interoperate with R6, S4, and Reference Classes

TypR does not replace R's OOP systems — it works alongside them. This guide shows how to use R6, S4, and RC objects from TypR code.

The key concept: Foreign<T>

R6, S4, and RC objects are opaque R values. TypR represents them with Foreign<Any>, a type that passes through the compiler untouched — no field access, no method calls, just values flowing through bindings and function arguments.

type R6Person <- Foreign<Any>;

This is not a limitation. It is the design: TypR does not try to model R's dynamic OOP systems inside its type checker. Instead, you wrap the OOP object in Foreign<Any> and delegate operations to R code.

Working with R6 objects

Suppose you have an R6 class in R/Person.R:

Person <- R6::R6Class("Person",
public = list(
name = NULL,
age = NULL,
initialize = function(name, age) {
self$name <- name
self$age <- age
},
greet = function() {
paste0("Hi, I'm ", self$name)
}
)
)

From TypR, declare the constructor and methods as signatures:

type R6Person <- Foreign<Any>;

@extern Person$new: (name: char, age: int) -> R6Person;

let p <- Person$new("Alice", 30);

For calling methods, use an extern escape hatch since method call syntax ($) is not typed:

extern (p: R6Person) -> char r#"p$greet()"#;

The r#"..."# raw R string is emitted verbatim into the generated .R file.

Working with S4 objects

S4 classes follow the same pattern — wrap the object in Foreign<Any> and use @extern for the generic functions:

type S4Model <- Foreign<Any>;

@extern stats::coef: (object: S4Model) -> Foreign<Any>;
@extern stats::summary: (object: S4Model) -> Foreign<Any>;

let m: S4Model <- readRDS("model.rds");
let s <- summary(m);

For creating S4 objects, use @extern with the constructor:

@extern methods::new: (Class: char, ...) -> Foreign<Any>;

Working with Reference Classes (RC)

RC objects are handled identically to R6 — they are just R values:

type RCLogger <- Foreign<Any>;

@extern Logger$new: () -> RCLogger;

let log <- Logger$new();

Using R {} blocks for complex OOP interactions

When you need to call multiple methods or access fields, R {} blocks are often cleaner than chaining extern escape hatches:

let result <- R {
person <- Person$new("Bob", 25)
paste(person$name, "is", person$age)
};

This block is emitted as-is into the generated R code. It is not type-checked, but it integrates seamlessly with the surrounding typed code.

Best practices

  1. Wrap, do not rebuild — Use Foreign<Any> for OOP objects. TypR is not meant to reimplement R's class systems.

  2. Signature the entry points — Declare @extern for constructors and key methods. Leave internal field access to R {} blocks.

  3. Keep OOP code in R files — If a class is complex, define it in R/ and only call it from TypR via signatures. This is the cleanest interop.

  4. Prefer R {} for multi-step OOP — When you need to create, configure, and call methods on an object in sequence, a raw R block is simpler than multiple extern escape hatches.

Full example

# Type declarations
type R6Person <- Foreign<Any>;
@extern Person$new: (name: char, age: int) -> R6Person;

# Create and use
let p <- Person$new("Alice", 30);

# Multi-step interaction via R block
let greeting <- R {
p$greet()
};

Where to go next