Historical Context & Motivation
Every programming language must decide how to represent and manipulate data in memory, and the choices a language's designers make about type systems profoundly shape its expressiveness, safety, and performance characteristics. R's type system descends from a lineage of statistical computing languages that prioritized interactive data analysis over systems-level concerns like manual memory management. Unlike C or Java, where a programmer explicitly declares every variable's type before use, R employs dynamic typing — the interpreter infers a value's type at runtime, and a single variable can be reassigned to hold a different type entirely. This design choice emerged directly from R's heritage: a language built by statisticians for statisticians, where rapid prototyping mattered more than compile-time guarantees.
The central question this lesson addresses is deceptively simple: when you assign x <- 42 in R, what exactly is x? Is it an integer, a floating-point number, something else? Understanding how R categorizes, stores, and implicitly converts between its atomic types is essential for debugging subtle errors — the kind where TRUE + 1 yields 2 and "3" + 1 throws an error, even though both seem like they should work.
Core Principles & Definitions
R's type system rests on a few foundational ideas that distinguish it from most general-purpose languages. The most important is that R has no true scalar types — even a single value like 5L is actually a vector of length one. Every atomic value in R belongs to one of six atomic vector types, four of which are the most commonly encountered in day-to-day programming: numeric (double), integer, character, and logical. The remaining two — complex and raw — serve specialized purposes and are less frequently used in typical data analysis workflows.
Everything Is a Vector
x <- 3.14 creates a numeric vector of length 1. This vectorization pervades the language: operations automatically broadcast across elements, eliminating explicit loops for elementwise computation.Atomic Homogeneity
c(), R silently coerces all elements to the most general type. This implicit coercion follows a strict hierarchy: logical → integer → double → character.Dynamic Typing, Strong Semantics
typeof() vs. class()
typeof() returns the internal storage mode (e.g., "double", "integer"), while class() returns the object-oriented class (e.g., "numeric", "factor"). Distinguishing these two queries is critical for debugging type-related issues.Visual Explanation — The Type Hierarchy
c(), R always coerces to the widest (rightmost) type present.The diagram above captures R's single most important typing rule: implicit coercion always moves toward the most general type. A logical value can be losslessly represented as an integer (TRUE becomes 1, FALSE becomes 0), an integer can be losslessly represented as a double (42L becomes 42.0), and any value can be losslessly represented as a character string. The reverse is not true — as.integer("hello") produces NA with a warning, because the string has no sensible integer representation. This asymmetry is precisely why coercion is one-directional: information can be preserved when widening, but may be lost when narrowing.
How R Stores Types Internally
Understanding R's type system at a deeper level requires examining how each atomic type maps to underlying C representations in R's interpreter. When you create a variable, R allocates a SEXP (S-expression pointer) — a C-level structure that contains a type tag (called SEXPTYPE), metadata (length, attributes), and a pointer to the data payload. The four primary SEXPTYPE values for our atomic types are LGLSXP (logical), INTSXP (integer), REALSXP (double/numeric), and STRSXP (character).
n is the vector length, sizeof(double) = 8, sizeof(integer) = 4, sizeof(logical) = 4. You can verify with object.size() in R. For a 1-million-element vector, using integer instead of double saves approximately 4 MB.42 as doubles (REALSXP), not integers. To create an integer, you must append the L suffix: 42L. This is a deliberate design choice — since R is primarily used for statistical computing, and most statistical operations produce real-valued results, defaulting to double avoids unnecessary integer-to-double coercions in the common case. Verify: typeof(42) returns "double", while typeof(42L) returns "integer".The practical implication of these storage details becomes significant at scale. If you are working with a dataset containing millions of rows and a column of small whole numbers (e.g., counts from 0 to 1000), storing them as integer rather than double halves the memory footprint for that column. Furthermore, integer arithmetic can be marginally faster on some architectures because the CPU can use integer-specific instructions. The object.size() function and the lobstr::obj_size() function from the lobstr package are your primary tools for profiling these memory differences.
Detailed Breakdown of Each Type
| Property | logical | integer | double (numeric) | character |
|---|---|---|---|---|
| typeof() | "logical" | "integer" | "double" | "character" |
| class() | "logical" | "integer" | "numeric" | "character" |
| Literal syntax | TRUE, FALSE, NA | 1L, 0L, NA_integer_ | 3.14, 1e−3, Inf, NaN | "abc", 'xyz', NA_character_ |
| Storage (bytes/elem) | 4 (int internally) | 4 (32-bit signed) | 8 (64-bit IEEE 754) | 8 (pointer to CHARSXP) |
| NA variant | NA | NA_integer_ | NA_real_ | NA_character_ |
| Test function | is.logical() | is.integer() | is.double() | is.character() |
c(TRUE, 3L, 2.5, "R"). Because character is the widest type present, all four elements are coerced to character strings before the vector is assembled. The bottom row shows the resulting SEXP structure: a STRSXP header followed by four pointers into R's global string cache.The detailed type comparison table above highlights several subtleties worth internalizing. First, note the asymmetry between typeof() and class() for doubles: typeof(3.14) returns "double" but class(3.14) returns "numeric". This is because "numeric" is an S3 class name that encompasses both doubles and integers in certain contexts, while "double" is the precise internal storage type. Second, each type has a typed NA variant: using NA_integer_ or NA_character_ ensures that a missing value does not inadvertently trigger coercion when inserted into a typed vector.
Worked Example — Type Inspection and Coercion
Suppose you are cleaning a dataset imported from a CSV file and need to verify that each column has the expected type before performing computations. The following worked example demonstrates a realistic type-checking and coercion workflow.
ages <- c("21", "34", "28", "N/A", "45")
Now inspect the type:typeof(ages) # "character"
class(ages) # "character"
is.character(ages) # TRUEmean(ages) without coercion.as.numeric() (equivalent to as.double()) to convert:
ages_num <- as.numeric(ages)
# Warning: NAs introduced by coercion
ages_num # 21 34 28 NA 45
The string "N/A" has no valid numeric representation, so R converts it to NA and emits a warning.typeof(ages_num) returns "double". The fourth element is now NA_real_.ages_int <- as.integer(ages_num)
ages_int # 21 34 28 NA 45
typeof(ages_int) # "integer"
We verify memory savings:object.size(ages_num) # 96 bytes
object.size(ages_int) # 72 bytesmask <- is.na(ages_int)
mask # FALSE FALSE FALSE TRUE FALSE
typeof(mask) # "logical"
Now leverage logical-to-integer coercion to count NAs:
sum(mask) # 1
The sum() function coerces TRUE → 1 and FALSE → 0, then sums.sum(is.na(x)) is one of the most common uses of logical-to-integer coercion in practice.mean(ages_int, na.rm = TRUE) # 32
The na.rm = TRUE argument removes NA values before computation. Without it, mean(ages_int) returns NA because R propagates missingness by default — a principled design choice that forces the analyst to make explicit decisions about missing data.Strengths, Pitfalls, and Common Gotchas
| Feature | Strength | Pitfall |
|---|---|---|
| Implicit coercion | Enables elegant idioms like sum(x > 0) to count TRUE values without explicit casting. | Silently converts types when combining vectors, potentially producing unexpected character vectors from mixed inputs. |
| Default double | Avoids integer overflow (R integers are 32-bit, max ≈ 2.1 × 10⁹) in most statistical operations. | Floating-point comparison issues: 0.1 + 0.2 == 0.3 returns FALSE. Use all.equal() instead. |
| NA propagation | Forces explicit handling of missing data, preventing silent computation on incomplete datasets. | Forgetting na.rm = TRUE causes entire summaries to return NA. Beginners often spend debugging time on this. |
| Dynamic typing | Rapid prototyping — no type declarations, fast iterative analysis at the REPL. | Type errors surface at runtime, not compile time. A function expecting numeric may silently receive character and fail deep in a pipeline. |
| Character string pool | Identical strings share memory via CHARSXP caching, making character vectors of repeated values very memory-efficient. | Unique strings (e.g., UUIDs) each allocate separate cache entries, which can cause unexpected memory bloat with millions of distinct strings. |
stopifnot(is.numeric(x)) assertions at function boundaries — a practice that becomes increasingly important as your R code moves from exploratory analysis to production pipelines.Connection to Advanced Type Constructs
The four atomic types covered in this lesson are the foundation upon which R builds its more complex data structures. Understanding this foundation makes the higher-level abstractions — factors, dates, data frames, tibbles — far less mysterious, because each of these is ultimately an atomic vector with attributes attached. A factor, for instance, is an integer vector with a levels attribute and a class attribute set to "factor". A Date is a double vector with class = "Date", storing the number of days since 1970-01-01.
| Concept | This Lesson (Atomic Types) | Advanced Topic |
|---|---|---|
| Homogeneous collection | Atomic vectors — all elements same type | Matrices & arrays — multi-dimensional atomic vectors |
| Heterogeneous collection | Not possible in a single atomic vector | Lists — each element can be any type; data frames are lists of equal-length vectors |
| Categorical data | Stored as character vectors | Factors — integer-backed with level labels, enabling ordered categories and efficient storage |
| Type safety | Runtime checks via is.*() functions | The vctrs package provides formal type hierarchies; R7/S7 classes add method dispatch with type contracts |
| Missing data | Typed NAs: NA_integer_, etc. | tidyr and data.table provide structured missing-data imputation workflows built on typed NAs |
As you move into subsequent lessons on lists, data frames, and the S3/S4 object systems, you will see that mastering atomic types is not merely an academic exercise — it is the prerequisite for understanding why df$column behaves the way it does, why certain joins fail in dplyr, and why Rcpp interfaces require you to declare exact SEXP types when passing data between R and C++.
Practice Problems
typeof(42) returns "double" rather than "integer", even though 42 is a whole number. What design rationale justifies this default, and how would you create an integer 42 instead?typeof() would return for each:
(a) TRUE + FALSE + TRUE
(b) c(1L, 2.5)
(c) paste(42, "is the answer")v <- c(TRUE, 3L, 4.5, "hello"). (a) What is typeof(v)? (b) What are the exact string values stored in v? (c) Now suppose you run as.numeric(v). What result do you get and why?status, should contain only TRUE or FALSE but was imported as character ("TRUE", "FALSE", and some "Yes"/"No" entries). Write R code that: (1) converts the column to logical, correctly handling "Yes" → TRUE and "No" → FALSE, (2) reports how many entries could not be converted, and (3) verifies the final type.Summary — R Data Types
R's type system is built on six atomic vector types, four of which are central to everyday programming: logical (TRUE/FALSE), integer (suffixed with L), double (the default for all numeric literals), and character (strings). R has no true scalars — every value is a vector of length one. When types are mixed in a single vector, R applies implicit coercion following the hierarchy logical → integer → double → character, always widening to the most general type present.
Use typeof() to query internal storage mode and class() for the S3 class; these can differ (e.g., typeof returns "double" while class returns "numeric"). Each type has a typed NA variant (NA_integer_, NA_real_, NA_character_) to prevent unintended coercion. Explicit coercion functions — as.integer(), as.double(), as.character(), as.logical() — give you precise control. Mastering these atomic types is the prerequisite for understanding every higher-level R data structure: factors, matrices, lists, and data frames are all built on this atomic vector foundation.