Skip to main content

Type System Reference

Complete reference for all types in Infr.

Primitive types

These map directly to R's atomic vector types:

Infr typeR typeofExamples
numericdouble3.14, 1e10
integerinteger1L, 2L
charactercharacter"hello"
logicallogicalTRUE, FALSE
complexcomplex1+2i
rawrawas.raw(0x1f)

All of these are vectors. numeric means "a numeric vector" (possibly of length 1).

Compound types

Lists

const mixed: list <- list(1, "a", TRUE)

Named lists (record-like)

const person: list<{name: character, age: numeric}> <- list(name = "Alice", age = 30)

person$name # character
person$age # numeric
person$email # Error: Field `email` does not exist

Data frames

const df: data.frame<{
id: integer,
name: character,
score: numeric
}> <- data.frame(
id = 1:3L,
name = c("Alice", "Bob", "Charlie"),
score = c(95.5, 87.0, 92.3)
)

df$score # numeric
df$nonexistent # Error: Column `nonexistent` does not exist

Sized vectors (scalar types)

Infr tracks the length of vectors returned by certain built-in functions. A scalar is a vector of length 1, written as T[1]:

ExpressionInferred typeNotes
length(x)integer[1]Always returns a scalar
nrow(df), ncol(df)integer[1]Always returns a scalar
sum(...), mean(x)numeric[1]Aggregation to scalar
min(...), max(...)numeric[1]Aggregation to scalar
any(...), all(...)logical[1]Aggregation to scalar
is.numeric(x), is.null(x), etc.logical[1]Type-checking predicates
which.min(x), which.max(x)integer[1]Index of extremum
nlevels(x)integer[1]Count of factor levels

Sized vectors are assignable to their base type (integer[1] is assignable to integer) and vice versa.

S3 class types (branded types)

Certain R functions return values with S3 classes that carry semantic meaning beyond their underlying storage type. Infr models these as branded types with a nominal barrier — an S3 class type is not assignable to its base type, preventing accidental misuse:

ExpressionTypeBase type
factor(x), as.factor(x)factorinteger
Sys.Date(), as.Date(x)Datenumeric
as.POSIXct(x)POSIXctnumeric
difftime(t1, t2)difftimenumeric
const today <- Sys.Date()       # type: Date
const n: numeric <- today # Error: Date is not assignable to numeric
const d2: Date <- today # OK

S3 class types with the same class name are assignable to each other.

Special types

TypeMeaning
anyOpts out of type checking. Escape hatch.
NULLThe R NULL value.
T?Nullable: T or NULL. Shorthand for T | NULL.
neverThe bottom type — no values. Returned by functions that never return (e.g. stop(), stopifnot()).

The never type is a subtype of every type, meaning it is assignable to anything. It vanishes from union types: never | T simplifies to T. This is useful for control flow — when a branch calls stop(), the type of an if/else expression is determined solely by the other branch.

Type aliases

Define reusable type names with type:

type Score = numeric
type MaybeString = character?
type StringOrNum = character | numeric
type Person = list<{name: character, age: numeric}>

Use them anywhere a type annotation is expected:

add_scores <- function(a: Score, b: Score) -> Score { a + b }
const alice: Person <- list(name = "Alice", age = 30)

Type aliases are erased during transpilation — they produce no R output.

Literal types

String, numeric, and integer literals can be used as types, representing exact values:

type Greeting = "hello" | "goodbye"
type Dice = 1 | 2 | 3 | 4 | 5 | 6
type Status = "active" | "inactive" | numeric

Literal types are subtypes of their base types — "hello" is assignable to character, but character is not assignable to "hello".

Union types

const value: numeric | character <- if (flag) 42 else "hello"

Function types

const transformer: (numeric) -> numeric <- function(x: numeric) -> numeric { x * 2 }

Both function(args) body and the R 4.1+ lambda shorthand \(args) body are supported:

const double <- \(x: numeric) -> numeric x * 2

Overloaded functions

Functions can have multiple signatures via .d.infr declaration files. When the same function name is declared multiple times, the signatures are merged into an overloaded type:

example.d.infr
#' @infr
as.character <- function(x: numeric) -> character
as.character <- function(x: logical) -> character
as.character <- function(x: complex) -> character

At call sites, Infr resolves the best matching overload based on argument types. If no overload matches, all available signatures are shown in the error message.

Coercion hierarchy

When combining values with c(), Infr follows R's implicit coercion hierarchy to determine the result type:

logical → integer → numeric → complex → character

The result type is the widest type in the hierarchy that covers all arguments:

c(TRUE, 1L)         # integer (logical coerced to integer)
c(1L, 2.5) # numeric (integer coerced to numeric)
c(1, 1+2i) # complex (numeric coerced to complex)
c(1, "a") # character (numeric coerced to character)
c(TRUE, "yes") # character (logical coerced to character)

tryCatch() return type

tryCatch() infers a union of the body expression type and all handler return types:

const result <- tryCatch(
readLines("file.txt"), # character
error = function(e) character(0) # character
)
# result type: character (union simplified since both are character)

const result2 <- tryCatch(
as.numeric("abc"), # numeric
warning = function(w) NA # numeric (NA)
)
# result2 type: numeric

If the body or a handler calls stop() (returning never), that branch vanishes from the union.

match.arg() inference

When a function parameter has a c("a", "b", "c") default and the function body calls match.arg() on that parameter, Infr infers the parameter type as a literal union:

greet <- function(style = c("formal", "casual", "excited")) {
style <- match.arg(style)
# style type: "formal" | "casual" | "excited"
}

greet("formal") # OK
greet("rude") # Error: "rude" is not assignable to "formal" | "casual" | "excited"

This matches the common R idiom for restricting string parameters.

Generics

Simple type parameters for functions:

identity <- function<T>(x: T) -> T { x }
c <- function<T>(...: T) -> T

Type narrowing

Infr narrows types inside conditional branches based on type-checking functions:

const x: numeric | character <- get_value()

if (is.numeric(x)) {
x + 1 # x is numeric here
}

const y: numeric? <- maybe_get()
if (!is.null(y)) {
y * 2 # y is numeric here
}

if (inherits(obj, "data.frame")) {
nrow(obj) # obj is data.frame here
}

Supported narrowing functions:

  • is.numeric(), is.character(), is.logical(), is.integer()
  • is.null() / !is.null()
  • inherits()