How to declare S3 and S4 generics in TypR
TypR can declare the types of R's S3 and S4 generic functions, giving you type safety when working with R's object-oriented systems.
S3 generics
S3 is R's simplest OOP system — a generic function dispatches on the class of
its first argument. Declare S3 generics with @ signatures:
@print: (x: Foreign<Any>) -> Empty;
@summary: (object: Foreign<Any>) -> Foreign<Any>;
@plot: (x: Foreign<Any>, ...) -> Empty;
@format: (x: Foreign<Any>, ...) -> char;
The Foreign<Any> type is used because S3 dispatch is dynamic — the actual
type is determined at runtime based on the class attribute.
Typing your own S3 generics
If your package defines an S3 generic:
# In R/my_generic.R
#' @export
my_generic <- function(x, ...) {
UseMethod("my_generic")
}
Declare it in TypR:
@my_generic: (x: Foreign<Any>, ...) -> Foreign<Any>;
Then implement methods in R/ as usual — TypR does not need to know about
the individual S3 methods.
S4 generics
S4 generics are more structured. Use @extern to declare them:
@extern stats::coef: (object: Foreign<Any>) -> Foreign<Any>;
@extern stats::confint: (object: Foreign<Any>, ...) -> Foreign<Any>;
@extern stats::fitted: (object: Foreign<Any>) -> Foreign<Any>;
@extern stats::residuals: (object: Foreign<Any>, ...) -> Foreign<Any>;
For your own S4 generics, declare them with @extern pointing to the package:
@extern mypackage::my_generic: (x: Foreign<Any>, ...) -> Foreign<Any>;
Using generics in typed code
Once declared, generics work naturally in typed functions:
@summary: (object: Foreign<Any>) -> Foreign<Any>;
@plot: (x: Foreign<Any>, ...) -> Empty;
let analyze <- fn(model: Foreign<Any>): char {
let s <- summary(model);
R { capture.output(plot(model)) }
"Analysis complete"
};
The Foreign<T> wrapper
Foreign<T> is the idiomatic type for opaque R values. It wraps any R object
that passes through TypR untouched:
type LmModel <- Foreign<Any>;
type Ggplot <- Foreign<Any>;
type R6Object <- Foreign<Any>;
Key properties of Foreign<T>:
- Values pass through
letbindings, function arguments, and return values without conversion m.field/m$fieldnever type-checks (no structural access)- You need a dedicated
@externaccessor for each field or method
Reference classes (RC)
RC generics are handled the same way as S3 — declare with @ or @extern:
@Logger$log: (msg: char) -> Empty;
@Logger$get_entries: () -> Foreign<Any>;
Practical pattern: typed model interface
type Model <- Foreign<Any>;
@extern stats::lm: (formula: char, data: Foreign<Any>) -> Model;
@extern stats::summary: (object: Model) -> Foreign<Any>;
@extern stats::coef: (object: Model) -> Foreign<Any>;
@extern stats::predict: (object: Model, newdata: Foreign<Any>) -> Foreign<Any>;
let fit_model <- fn(formula: char, data: Foreign<Any>): Model {
stats::lm(formula, data)
};
let get_coefficients <- fn(model: Model): Foreign<Any> {
stats::coef(model)
};
Best practices
-
Use
Foreign<Any>for dispatch types — S3/S4 dispatch is dynamic. Do not try to model the class hierarchy in TypR's type system. -
Declare signatures for generics you call — Even partial type safety (checking the first argument) is better than none.
-
Keep method implementations in R — TypR is for typed logic, not for reimplementing OOP dispatch. Write methods in
R/, declare generics in.ty. -
Use
@externfor cross-package generics — When calling generics from other packages (stats, ggplot2, etc.),@externis the right tool.
Where to go next
- Interfaces & Structural Validation — TypR's own structural polymorphism
- Signatures, @extern & Foreign — full reference
- Escape Hatches —
R {},extern,function()