Type Inference
Type inference is the compiler's ability to work out the type of a variable or expression on its own, from the values assigned to it and from how it is used, so that the type does not have to be written in the source. Rust is statically typed, and inference does not change that: every type is still fixed at compile time. Inference only removes the need to spell out types the compiler can already deduce.
use std::any::type_name_of_val;
fn main() {
let a = 5;
let b = 2.5;
let c = 'x';
let d = "text";
let e = true;
let f = [1, 2, 3];
let g = (1, 2.0, 'c');
println!("{}", type_name_of_val(&a));
println!("{}", type_name_of_val(&b));
println!("{}", type_name_of_val(&c));
println!("{}", type_name_of_val(&d));
println!("{}", type_name_of_val(&e));
println!("{}", type_name_of_val(&f));
println!("{}", type_name_of_val(&g));
}
i32
f64
char
&str
bool
[i32; 3]
(i32, f64, char)
None of these variables has an annotation, and each has a definite type.
How Inference Works
The compiler gives every expression whose type is not yet known a type variable, written ?T in this section. It then reads the code and records constraints: "this value is passed to a function that takes a u64", "this method is only defined on Vec<_>", "both branches of this if must match". Each constraint narrows a type variable. When every variable has been narrowed to one concrete type, inference succeeds. If a variable is still undetermined at the end, or if two constraints contradict each other, compilation fails.
let mut items = Vec::new(); items: Vec<?T> ?T is unknown
items.push(7u8); push needs ?T = u8 ?T = u8
items: Vec<u8> resolved
Constraints can come from anywhere in the function body, including code that is written after the variable is declared. Information flows both forwards (from a value to the variable that holds it) and backwards (from a later use to the earlier declaration).
Inference From the Initial Value
The type of a literal or expression on the right of let becomes the type of the variable:
| Initializer | Inferred type |
|---|---|
5 |
i32 (default for an integer literal) |
2.5 |
f64 (default for a float literal) |
'x' |
char |
"text" |
&str |
true |
bool |
String::from("a") |
String |
vec![1u8, 2] |
Vec<u8> |
(1, 2.0) |
(i32, f64) |
Inference From Later Use
An integer literal with no other constraint becomes i32. If the variable is later used where another integer type is required, that requirement wins:
use std::any::type_name_of_val;
fn takes_u64(n: u64) -> u64 {
n * 2
}
fn main() {
let x = 10;
let result = takes_u64(x);
println!("{} {}", type_name_of_val(&x), result);
let mut items = Vec::new();
println!("pushing...");
items.push(7u8);
println!("{} {:?}", type_name_of_val(&items), items);
}
u64 20
pushing...
alloc::vec::Vec<u8> [7]
x is a u64, not an i32, because passing it to takes_u64 constrained it. items has no element type at the point of Vec::new(). The call to push later fixes it as u8.
The i32 and f64 defaults apply only when nothing else constrains a literal. These fallback rules are covered in the section on numeric type inference.
Contradicting Constraints
A variable has one type, so uses that demand different types conflict. The first use fixes the type, and the second use is the one the compiler reports:
// ❌ does not compile
fn main() {
let x = 5;
let small: u8 = x;
let large: u16 = x;
println!("{small} {large}");
}
error[E0308]: mismatched types
--> main.rs:5:22
|
5 | let large: u16 = x;
| --- ^ expected `u16`, found `u8`
| |
| expected due to this
|
help: you can convert a `u8` to a `u16`
|
5 | let large: u16 = x.into();
| +++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
Deferred Initialization
A variable can be declared without a value and assigned later. The type is inferred from the assignments:
fn main() {
let late;
let flag = true;
if flag {
late = 1.5;
} else {
late = 2.5;
}
println!("{late}");
}
1.5
Inference Stays Inside a Function Body
Inference works on the body of one function at a time. It never looks at how a function is called to determine the types of its parameters, and it never looks at the body to determine the return type. Parameter types and return types must be written:
// ❌ does not compile
fn scale(value) -> i32 {
value * 2
}
fn main() {
println!("{}", scale(2));
}
error: expected one of `:`, `@`, or `|`, found `)`
--> main.rs:2:15
|
2 | fn scale(value) -> i32 {
| ^ expected one of `:`, `@`, or `|`
|
help: if this is a parameter name, give it a type
|
2 | fn scale(value: TypeName) -> i32 {
| ++++++++++
help: if this is a type, explicitly ignore the parameter name
|
2 | fn scale(_: value) -> i32 {
| ++
error: aborting due to 1 previous error
The same holds for struct fields, const items, and static items. These boundaries make every function's contract visible in its signature and keep type errors local: a mistake inside one function cannot change what the compiler decides about another function.
When Annotations Are Required
When the constraints do not pin down a type, the compiler stops with an error asking for an annotation. It never picks an arbitrary type.
A Container With No Element Type
// ❌ does not compile
fn main() {
let items = Vec::new();
println!("{}", items.len());
}
error[E0282]: type annotations needed for `Vec<_>`
--> main.rs:3:9
|
3 | let items = Vec::new();
| ^^^^^ ---------- type must be known at this point
|
help: consider giving `items` an explicit type, where the type for type parameter `T` is specified
|
3 | let items: Vec<T> = Vec::new();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0282`.
Nothing says what the vector holds, and len() does not depend on the element type, so the type variable stays unresolved.
A Generic Return Type
Some functions are generic over their return type, so the caller has to say which type is wanted. parse and collect are the common cases:
// ❌ does not compile
fn main() {
let number = "42".parse().unwrap();
println!("{number}");
}
error[E0284]: type annotations needed
--> main.rs:3:9
|
3 | let number = "42".parse().unwrap();
| ^^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
= note: the type must also implement `FromStr`
note: required by a bound in `core::str::<impl str>::parse`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/str/mod.rs:2782:4
help: consider giving `number` an explicit type
|
3 | let number: /* Type */ = "42".parse().unwrap();
| ++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0284`.
Text such as "42" can be parsed into u8, i64, f64, and many other types. The target type has to come from somewhere.
// ❌ does not compile
fn main() {
let numbers = (1..4).collect();
println!("{}", numbers.len());
}
error[E0282]: type annotations needed
--> main.rs:3:9
|
3 | let numbers = (1..4).collect();
| ^^^^^^^
4 | println!("{}", numbers.len());
| ------- type must be known at this point
|
help: consider giving `numbers` an explicit type
|
3 | let numbers: Vec<_> = (1..4).collect();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0282`.
collect can build a Vec, a HashSet, a String, a HashMap, and more.
A Method Call on a Type That Is Not Known Yet
A method call needs the type of the receiver to already be known at that point in the code. The compiler resolves methods in order, so a later constraint that would settle the type does not help:
// ❌ does not compile
fn main() {
let x = "5".parse().unwrap();
println!("{}", x.pow(2));
}
error[E0282]: type annotations needed
--> main.rs:3:9
|
3 | let x = "5".parse().unwrap();
| ^
4 | println!("{}", x.pow(2));
| - type must be known at this point
|
help: consider giving `x` an explicit type
|
3 | let x: /* Type */ = "5".parse().unwrap();
| ++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0282`.
A numeric literal that has no concrete type yet causes the same problem when a method is called on it:
// ❌ does not compile
fn main() {
let x = 2.0;
println!("{}", x.powi(2));
}
error[E0689]: can't call method `powi` on ambiguous numeric type `{float}`
--> main.rs:4:22
|
4 | println!("{}", x.powi(2));
| ^^^^
|
help: you must specify a type for this binding, like `f32`
|
3 | let x: f32 = 2.0;
| +++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0689`.
{float} is the compiler's name for a float literal whose type is still open. The default of f64 is applied only at the end of type checking the function body, which is too late for the method lookup on powi. Writing 2.0_f64, or declaring let x: f64 = 2.0;, fixes it.
Giving the Compiler the Missing Type
There are four ways to supply a type.
A Type Annotation on the Variable
fn main() {
let a: i32 = "42".parse().unwrap();
let squares: Vec<i32> = (1..=4).map(|n| n * n).collect();
println!("{a} {squares:?}");
}
42 [1, 4, 9, 16]
The Turbofish
The turbofish, ::<Type>, supplies the generic argument directly on the function or method call. It is useful when there is no variable to annotate, such as in the middle of an expression:
fn main() {
let b = "42".parse::<u8>().unwrap();
let c = "2.5".parse::<f64>().unwrap();
let total = [1, 2, 3].iter().sum::<i32>();
println!("{b} {c} {total}");
}
42 2.5 6
The Placeholder _
The _ placeholder tells the compiler to infer part of a type while the rest is written out. It works in annotations and in turbofish arguments:
fn main() {
let words = "a b c".split(' ').collect::<Vec<_>>();
let parsed: Vec<_> = ["1", "2"]
.iter()
.map(|s| s.parse::<i64>().unwrap())
.collect();
println!("{words:?} {parsed:?}");
}
["a", "b", "c"] [1, 2]
Vec<_> fixes the container and leaves the element type to inference. This is the most common way to annotate collect().
A Constraint From Somewhere Else
Any use that fixes the type is enough, such as a function parameter, a struct field, or an annotated return type:
fn main() {
let total: u64 = [1, 2, 3].iter().sum();
let default: String = Default::default();
let widened: i64 = 5i32.into();
println!("{total} {default:?} {widened}");
}
6 "" 5
Inference and Closures
Closure parameters and return types are normally inferred, because a closure is written inside a function body:
fn main() {
let add_one = |n| n + 1;
println!("{}", add_one(41u8));
}
42
A closure is not generic. Its parameter types are fixed by the first use, and a later call with a different type is an error:
// ❌ does not compile
fn main() {
let identity = |x| x;
println!("{}", identity(5));
println!("{}", identity("five"));
}
error[E0308]: mismatched types
--> main.rs:5:29
|
5 | println!("{}", identity("five"));
| -------- ^^^^^^ expected integer, found `&str`
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `{integer}`
--> main.rs:4:29
|
4 | println!("{}", identity(5));
| -------- ^ expected because this argument is of type `{integer}`
| |
| in this closure call
note: closure parameter defined here
--> main.rs:3:21
|
3 | let identity = |x| x;
| ^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
Seeing an Inferred Type
When the inferred type is not obvious, there are three ways to check it.
- Editor hints.
rust-analyzershows inferred types as inlay hints next to the variable. std::any::type_name_of_val. Prints the type at run time, as in the examples above.- A deliberate mismatch. Assigning the value to the unit pattern
()fails, and the error message names the real type:
// ❌ does not compile
fn main() {
let names = vec![String::from("Ada")];
let () = names;
}
error[E0308]: mismatched types
--> main.rs:4:9
|
4 | let () = names;
| ^^ ----- this expression has type `Vec<String>`
| |
| expected `Vec<String>`, found `()`
|
= note: expected struct `Vec<String>`
found unit type `()`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
When to Annotate Anyway
Inference makes annotations optional in many places, not forbidden. An annotation is worth writing when:
- The compiler requires it, as in the cases above.
- The default would be wrong.
let id = 7;is ani32, but if the value is an identifier that can exceed 2 billion,let id: u64 = 7;states the intent. - It documents a non-obvious type, such as the result of a long iterator chain.
- It turns a distant, confusing error into a local one. Annotating a variable makes the compiler report a mismatch at the line where the value is created.