| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo only if Terraform is not installed yet |
| Scope | Terraform primitive, collection, and structural type constraints, list vs set, map vs object, optional object attributes, any, null, automatic and explicit type conversion, terraform console type() debugging, and common type errors. Does not cover variable precedence, for_each tutorials, full function catalogs, JSON, dynamic blocks, or provider schemas. |
| Related guides | Terraform variables Terraform locals Terraform expressions Terraform functions Terraform Associate certification course |
Every value Terraform evaluates has a type. Input variables can declare a type constraint, while locals and expressions usually have their types inferred from the values they produce.
When a supplied value cannot be converted to a required type constraint, Terraform reports an error before infrastructure changes are applied. Knowing the type system helps you design variables, write expressions, and pass the right shapes into for_each later.
Primitive
├── string
├── number
└── bool
Collection
├── list(...)
├── set(...)
└── map(...)
Structural
├── tuple([...])
└── object({...})This guide walks through each family with accepted and rejected values on Terraform 1.15.8.
~/terraform-labs/terraform-data-types/ on the Terraform lab environment on Ubuntu. Run terraform init in each subdirectory before plan or console. Examples use the built-in terraform_data resource so you do not need cloud credentials.
Primitive Terraform types
Primitives are single values with no internal structure.
| Type | Example literal | Typical use |
|---|---|---|
string |
"api" |
Names, labels, ARNs |
number |
2, 3.14 |
Counts, ports, sizes |
bool |
true, false |
Feature flags, enable/disable |
The main lab at ~/terraform-labs/terraform-data-types/main/ declares all three:
variable "label" {
type = string
default = "api"
}
variable "replicas" {
type = number
default = 2
}
variable "enabled" {
type = bool
default = true
}Initialize the lab, then apply so Terraform evaluates the declared types in the configuration:
cd ~/terraform-labs/terraform-data-types/main && terraform initApply the configuration to confirm the primitive variables load with the expected types:
cd ~/terraform-labs/terraform-data-types/main && terraform apply -auto-approve -input=false -no-colorThe primitives output block confirms all three types loaded:
Outputs:
primitives = {
"enabled" = true
"label" = "api"
"replicas" = 2
}Supplying a string where a number is expected fails before plan. The errors/string-for-number/ directory sets default = "two" on a number variable:
cd ~/terraform-labs/terraform-data-types/errors/string-for-number && terraform validateError: Invalid default value for variable
on main.tf line 7, in variable "instance_count":
7: default = "two"
This default value is not compatible with the variable's type constraint: a
number is required.Collection types: list, set, and map
Collection types group multiple values. Each collection kind enforces different rules about duplicates, order, and key structure.
list type
A list(T) preserves element order and allows duplicates:
variable "subnet_ids" {
type = list(string)
default = ["subnet-a", "subnet-b"]
}Index the first element with bracket notation:
cd ~/terraform-labs/terraform-data-types/main && echo 'var.subnet_ids[0]' | terraform console"subnet-a"set type
A set(T) requires unique elements. Order is not meaningful:
variable "unique_zones" {
type = set(string)
default = ["zone-a", "zone-b"]
}When you assign a list literal with duplicates to a set variable, Terraform deduplicates at assignment time. The list-vs-set/ lab makes that visible:
variable "as_list" {
type = list(string)
default = ["a", "b", "a"]
}
variable "as_set" {
type = set(string)
default = ["a", "b", "a"]
}Plan from that directory and compare output lengths:
cd ~/terraform-labs/terraform-data-types/list-vs-set && terraform plan -input=false -no-color+ input = {
+ list_len = 3
+ set_len = 2
}
Changes to Outputs:
+ as_list = [
+ "a",
+ "b",
+ "a",
]
+ as_set = [
+ "a",
+ "b",
]The list keeps three elements; the set stores two unique values.
Convert a list to a set explicitly when you need uniqueness in an expression:
cd ~/terraform-labs/terraform-data-types/list-vs-set && echo 'toset(["a","b","a"])' | terraform consoletoset([
"a",
"b",
])toset() collapses duplicates the same way a set variable does. That matters when you build a set of keys for for_each from a list that might repeat values.
map type
A map(T) uses string keys; every value must share the same element type:
variable "tags" {
type = map(string)
default = {
env = "dev"
team = "platform"
}
}Look up a value by key:
cd ~/terraform-labs/terraform-data-types/main && echo 'var.tags["env"]' | terraform console"dev"Passing a bare string to a set(string) variable fails type checking. From errors/list-for-set/ with zone_ids = "not-a-set" in terraform.tfvars:
cd ~/terraform-labs/terraform-data-types/errors/list-for-set && terraform plan -input=false -no-colorError: Invalid value for input variable
on terraform.tfvars line 1:
1: zone_ids = "not-a-set"
The given value is not suitable for var.zone_ids declared at
main.tf:5,1-20: set of string required, but have string.Structural types: tuple and object
Structural types describe fixed shapes. They differ from collections in how strictly Terraform checks each position or attribute.
tuple type
A tuple([T1, T2, ...]) fixes the number and type of each position:
variable "endpoint" {
type = tuple([string, number, bool])
default = ["10.0.0.1", 443, true]
}A list has one element type shared by every element, while a tuple can assign a different type to each position. Terraform can automatically convert compatible list and tuple values when the destination constraint allows it. The tuple([string, number, bool]) example above fixes position two as a number and position three as a bool.
Omitting a position fails validation. In errors/tuple-mismatch/, the default has only two elements for a three-position tuple:
cd ~/terraform-labs/terraform-data-types/errors/tuple-mismatch && terraform validateError: Invalid default value for variable
on main.tf line 7, in variable "endpoint":
7: default = ["10.0.0.1", 443]
This default value is not compatible with the variable's type constraint:
tuple required.object type
An object({...}) names attributes, each with its own type:
variable "app" {
type = object({
name = string
enabled = bool
ports = list(number)
})
default = {
name = "web"
enabled = true
ports = [80, 443]
}
}Access attributes with dot notation:
cd ~/terraform-labs/terraform-data-types/main && echo 'var.app.name' | terraform console"web"Omitting a required attribute fails. From errors/malformed-object/:
cd ~/terraform-labs/terraform-data-types/errors/malformed-object && terraform validateError: Invalid default value for variable
on main.tf line 10, in variable "app":
10: default = {
11: name = "web"
12: }
This default value is not compatible with the variable's type constraint:
attribute "enabled" is required.map vs object
map(T) |
object({...}) |
|
|---|---|---|
| Keys | Any string keys at runtime | Fixed attribute names in the type |
| Value types | All values share one type T |
Each attribute can have a different type |
| Typical use | Tags, labels, arbitrary string maps | Structured config with known fields |
Use map(string) for { env = "dev", team = "platform" }. Use object({ name = string, ports = list(number) }) when the schema is fixed and types differ per field.
Optional object attributes
Terraform 1.3+ supports optional() inside object type constraints. Callers can omit marked attributes; Terraform fills a default or null:
variable "service" {
type = object({
name = string
description = optional(string, "default desc")
labels = optional(map(string), {})
})
default = { name = "svc" }
}After apply, omitted attributes receive their defaults:
cd ~/terraform-labs/terraform-data-types/main && echo 'var.service.description' | terraform console"default desc"The optional labels map defaults to an empty map when omitted:
cd ~/terraform-labs/terraform-data-types/main && echo 'var.service.labels' | terraform consoletomap({})Only name was supplied in the default; description and labels came from optional() defaults.
any, null, and type conversion
Using type = any
type = any lets Terraform infer a suitable concrete type for the supplied value rather than requiring you to specify that type in advance:
variable "flexible" {
type = any
default = { key = "value" }
}any is not itself a type and does not turn off type checking. Once Terraform infers a concrete type from the assigned value, downstream expressions still must be compatible. Prefer explicit constraints when your module inspects the structure; reserve any for values you treat as opaque data.
null and types
null means absence of a value. Nullable variables (nullable = true, the default) allow null on optional inputs. In expressions, null propagates until a function or operator rejects it.
null itself is a valid console result:
cd ~/terraform-labs/terraform-data-types/main && echo 'null' | terraform consolenullAutomatic and explicit conversion
Terraform converts between compatible types in some contexts. Explicit conversion functions make the intent clear:
| Function | Example | Result |
|---|---|---|
tostring |
tostring(42) |
"42" |
tonumber |
tonumber("42") |
42 |
tobool |
tobool("true") |
true |
tolist |
tolist(["a"]) |
["a"] |
toset |
toset(["a","b"]) |
set of two strings |
tomap |
tomap({ k = "v" }) |
{ "k" = "v" } |
Run conversions in the conversions/ directory console:
cd ~/terraform-labs/terraform-data-types/conversions && echo 'tostring(42)' | terraform console"42"Numeric strings parse back to numbers with tonumber:
cd ~/terraform-labs/terraform-data-types/conversions && echo 'tonumber("42")' | terraform console42Not every string converts. tonumber("not-a-number") fails in console:
cd ~/terraform-labs/terraform-data-types/main && echo 'tonumber("not-a-number")' | terraform consoleError: Invalid function argument
on <console-input> line 1:
(source code not available)
Invalid value for "v" parameter: cannot convert "not-a-number" to number;
given string must be a decimal representation of a number.Full function semantics live in the Terraform functions lesson.
Inspect types with terraform console
When plan errors mention incompatible types, the type() function in terraform console shows what Terraform inferred. It is a debugging aid only; you cannot call type() in .tf files.
cd ~/terraform-labs/terraform-data-types/main && echo 'type(var.subnet_ids)' | terraform consolelist(string)Sets report their element type separately from lists:
cd ~/terraform-labs/terraform-data-types/main && echo 'type(var.unique_zones)' | terraform consoleset(string)Object types list every attribute and its constraint:
cd ~/terraform-labs/terraform-data-types/main && echo 'type(var.app)' | terraform consoleobject({
enabled: bool,
name: string,
ports: list(number),
})The type() output for var.flexible shows object({ key: string }) even though the variable uses type = any, because Terraform inferred the shape from the default value.
Common Terraform type errors
Reproduce each failure in an isolated subdirectory under ~/terraform-labs/terraform-data-types/errors/.
| Symptom | Likely cause | Fix |
|---|---|---|
a number is required |
String assigned to number |
Pass a numeric literal or use tonumber() on a valid decimal string |
set of string required, but have string |
Scalar passed where a collection was expected | Wrap in toset([...]) or fix the tfvars shape |
attribute "enabled" is required |
Object missing a required field | Add the attribute or mark it optional() |
tuple required |
List length or element types do not match the tuple | Match every position type and count exactly |
cannot convert ... to number |
tonumber() on a non-numeric string |
Validate input before conversion |
list vs set mismatch in an expression |
Mixed collection kinds in an operation | Convert with tolist() or toset() to align types |
The errors/invalid-conversion/ directory triggers the tonumber failure at validate time inside a locals block:
cd ~/terraform-labs/terraform-data-types/errors/invalid-conversion && terraform validateError: Invalid function argument
on main.tf line 6, in locals:
6: bad_number = tonumber("not-a-number")
├────────────────
│ while calling tonumber(v)
Invalid value for "v" parameter: cannot convert "not-a-number" to number;
given string must be a decimal representation of a number.References
- Type Constraints — Terraform documentation
- Types and Values — Terraform configuration language
- Conversion Functions — Terraform documentation
- type function — Terraform documentation
Summary
You walked through Terraform types from primitives through collections and structural shapes. Lists preserve order and allow duplicates; sets enforce uniqueness; maps hold string keys with uniform value types; tuples fix position-level types; objects name attributes with per-field types.
The comparisons that matter most in daily work are list versus set (duplicates and for_each keys) and map versus object (dynamic homogeneous keys versus fixed heterogeneous attributes). Optional object attributes let callers omit fields without widening the entire variable to any.
When a type error message is vague, terraform console with type() shows the inferred constraint. Explicit conversion functions make transforms readable, but they cannot salvage invalid input such as tonumber("not-a-number"). Next, apply these constraints in Terraform expressions or review Terraform variables when wiring types to tfvars and defaults.

