Type checking
Type checking finds the mistakes R reports only at run time: a string passed where a number is
needed, a misspelled list field, a NULL that nobody handled. You switch it on in ry.toml, because
an existing codebase can have many such findings and you should choose when to face them:
[check]typing = trueNothing to annotate
Section titled “Nothing to annotate”ry infers types from how values are used, so unannotated code is checked as it is:
apply_discount <- function(price, rate) price * (1 - rate)apply_discount(100, "0.2")type-mismatch
x expected a numeric value (`integer` or `double`), found `character` --[R/discount.R:2:21] 1 | apply_discount <- function(price, rate) price * (1 - rate) 2 | apply_discount(100, "0.2") | ^^^^^- and * are arithmetic, so both parameters must be numbers. Inference runs even with type errors
switched off, because hover, completion, and inlay hints in your editor are built on it.
A function is as general as its body allows, and hover shows the inferred type:
identity2 <- function(x) x # <T> fn(x: T) -> Tincrement <- function(x) x + 1L # <T: numeric> fn(x: T) -> TT stands for any type, and T: numeric for integer or double, so increment(2.5) is a
double and increment("a") is an error.
Annotations
Section titled “Annotations”Inference cannot know what a function is meant to accept, or what a value read from a file contains. An annotation states that type, and ry then checks it against the value and against every use, so a wrong call is reported at the call instead of deep inside the function.
You can write an annotation as a #: comment directly above the assignment, which keeps the file
plain R that every tool can run, or inline, in the code itself. Both spellings mean the same, and
every typed example on this site offers both in tabs, with the tab you pick remembered:
#: fn(price: double, rate: double) -> doubleapply_rate <- function(price, rate) price * rate
#: doublerate <- readRDS("rate.rds")
apply_rate(rate, "x")apply_rate <- function(price: double, rate: double) -> double price * rate
rate: double <- readRDS("rate.rds")
apply_rate(rate, "x")type-mismatch
x expected `double`, found `character`The body is checked too, so a function declared -> double that returns a string is an error at the
return value. Inference already covers the inside of a function, so annotate where code meets other
code: exported functions, values read from files, and anything whose contract you want enforced.
Plain Rscript cannot run inline annotations, so use the comments while a file must also run there;
inline annotations is the step that switches.
Functions
Section titled “Functions”Once a function has a signature, ry checks every call against it: the types, the argument names, and
the number of arguments. Parameter names are part of the type, because R matches arguments by name
as often as by position. A parameter callers may omit must have a default: inline, the default
itself marks it as optional, and in a comment you write [greeting]:
#: fn(name: character, [greeting]: character) -> charactergreet <- function(name, greeting = "hello") { paste(greeting, name)}greet("Ada", greting = "hi")greet <- function(name: character, greeting: character = "hello") -> character { paste(greeting, name)}greet("Ada", greting = "hi")type-mismatch
x this function has no parameter `greting`. Did you mean `greeting`?...: character types the extra arguments, so fn(...: character) -> character checks that every
argument is a string. A function with ... accepts any argument name, so a misspelled name is not
reported there, just as R does not report it. A generic function names its type parameters, as in
#: <T> fn(condition: logical, value: T) -> T | NULL or inline
function<T>(condition: logical, value: T) -> T | NULL.
Your own functions have exactly one signature. To accept several shapes, use a union such as
integer | character. A long signature can be written one parameter per line, in the style of
roxygen, with #: @param count {integer} and #: @return {integer} lines. The
type system reference has the full grammar.
Adopting it one file at a time
Section titled “Adopting it one file at a time”A comment in a file, usually at the top, overrides the project setting for that file:
# typing: onThe values are on, off, and strict (which also requires a type everywhere, see
strict mode), so you can switch type checking on file by file, or keep
it on for the project and switch it off where a file is not ready. To silence one type finding, name
its code in a comment on the line, as in # ry: allow(type-mismatch)
(Silencing one finding).
What ry cannot model, such as data.frame columns and R6 objects, gets the type Unknown, which is
compatible with everything, so a clean run proves less than it seems.
Unknown values explains how to find and close these gaps.