# Type your code

**Goal:** find type errors in plain R before it runs. You switch type checking on, read what
inference finds, and add annotations where code meets other code. The checker never runs your code,
and the annotations are comments, so the code keeps running on GNU R.

## Switch it on

```toml
# ry.toml
[check]
typing = true
```

A `# typing: on` comment at the top of a file switches it on for that file alone, and
`# typing: off` switches it off, so you can adopt types one file at a time.

## Inference

ry infers types from how values are used, so unannotated code is checked as it is:

```r
apply_discount <- function(price, rate) price * (1 - rate)
apply_discount(100, "0.2")
```

```text
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 `*` need numbers, so both parameters must be numbers. Each function gets the most general
type its body allows, which the editor shows on hover:

```r
identity2 <- function(x) x           # <T> fn(x: T) -> T
increment <- function(x) x + 1L      # <T: numeric> fn(x: T) -> T
```

`T` is any type, and `T: numeric` is `integer` or `double`, so `increment(2.5)` is a `double` and
`increment("a")` is an error. Argument names are part of a function's type, because R matches
arguments by name as often as by position:

```r
greet <- function(name, greeting = "hello") paste(greeting, name)
greet("Ada", greting = "hi")
```

```text
type-mismatch

  x this function has no parameter `greting`. Did you mean `greeting`?
```

## Annotate

Inference cannot know what a file holds or what a function promises. A `#:` comment directly above
an assignment states it, and every use of the value is checked against it. In the **Untyped** tab,
`settings` could be anything, so ry reports nothing:

### Untyped

```r
settings <- readRDS("settings.rds")

should_retry <- function(attempt) attempt < settings$retires
```

### Comments

```r
#: list{retries: integer, verbose: logical}
settings <- readRDS("settings.rds")

#: fn(attempt: integer) -> logical
should_retry <- function(attempt) attempt < settings$retires
```

```text
type-mismatch

  x field `retires` does not exist in `list{retries: integer, verbose: logical}`. Did you mean `retries`?
```

### Inline

```r
settings: list{retries: integer, verbose: logical} <- readRDS("settings.rds")

should_retry <- function(attempt: integer) -> logical attempt < settings$retires
```

```text
type-mismatch

  x field `retires` does not exist in `list{retries: integer, verbose: logical}`. Did you mean `retries`?
```

The **Inline** tab writes each type next to its name. It means the same to the checker, but GNU R
cannot parse it, so it runs only through ry ([step 3](/tutorial/runtime.md#inline-annotations)). Your
tab choice holds on every page.

The body is checked against the signature too: a function declared `-> logical` that returns a
string is an error at the return value. `[name]` in a `#:` signature marks a parameter callers may
omit, as in `#: fn(name: character, [greeting]: character) -> character`. Annotate where code meets
other code: exported functions, values read from files, and any contract you want enforced.
Inference covers the rest.

## Shapes

R has no scalars, and one list type serves as record, tuple and dictionary alike. ry's types tell
these apart without any change to your code:

| Type | Holds | Example |
| --- | --- | --- |
| `integer` | exactly one element | `1L` |
| `integer[]` | any length | `c(1L, 2L)` |
| `integer[named]` | any length, with names | `c(a = 1L)` |
| `list{name: character, age: integer}` | fixed fields | `list(name = "Ada", age = 36L)` |
| `list{integer, character}` | fixed positions | `list(1L, "ok")` |
| `list[integer]` | any length | `lapply(words, nchar)` |
| `list[named: integer]` | looked up by key | `[[key]]` gives `integer \| NULL` |
| `character \| NULL` | either | `NULL` default |

`double` accepts `integer` and `logical`, but a number never passes for `character`. A fixed shape
makes indexing checkable, where R returns `NULL` for a misspelled field and stops only at run time
for a position past the end:

```r
result <- list(200L, "ok")
result[[3]]
```

```text
type-mismatch

  x position 3 does not exist in `list{integer, character}`
```

## NULL

A value that may be `NULL` has a union type, and using it as if it were not is an error:

### Comments

```r
#: fn(config: list{retries: integer} | NULL) -> integer
retries <- function(config) {
  config$retries
}
```

### Inline

```r
retries <- function(config: list{retries: integer} | NULL) -> integer {
  config$retries
}
```

```text
type-mismatch

  x expected `integer`, found `integer | NULL`
```

An `is.null()` test *narrows* the type for the rest of the function, so adding
`if (is.null(config)) return(3L)` as the first line clears the error. `is.character()`,
`is.numeric()` and the other `is.*` tests narrow the same way.

## Unknown

Values ry cannot model have the type `Unknown`, which fits everywhere, so one unmodeled construct
never causes a cascade of false errors. The main sources are base `data.frame` columns, S4 and R6
objects, and packages ry has no stub for. An annotation gives such a value a type, as `settings`
got one [above](#annotate), and every later use is checked against it.

ry ships types for base R, the default packages, `data.table`, `dplyr`, `ggplot2` and `testthat`,
and export lists for the rest of the tidyverse and other common packages. For any other package, a
stub file declares what you call, and turns the unresolved-name check back on:

```text
# stubs/dbclient.Rtypes
@type Session
connect : fn(host: character) -> Session
```

[Authoring stubs](/development/authoring-stubs.md) has the format. A clean run does not say how much
was `Unknown`; [strict mode](/tutorial/fully-typed.md#strict-mode) does.
[Limitations](/reference/type-system.md#limitations) lists what is not checked.

**Next:** [run on ry's runtime](/tutorial/runtime.md).
