Terraform Data Types: List, Set, Map, Tuple and Object

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.

text
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.

NOTE
Work in ~/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:

hcl
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:

bash
cd ~/terraform-labs/terraform-data-types/main && terraform init

Apply the configuration to confirm the primitive variables load with the expected types:

bash
cd ~/terraform-labs/terraform-data-types/main && terraform apply -auto-approve -input=false -no-color

The primitives output block confirms all three types loaded:

output
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:

bash
cd ~/terraform-labs/terraform-data-types/errors/string-for-number && terraform validate
output
Error: 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:

hcl
variable "subnet_ids" {
  type    = list(string)
  default = ["subnet-a", "subnet-b"]
}

Index the first element with bracket notation:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'var.subnet_ids[0]' | terraform console
output
"subnet-a"

set type

A set(T) requires unique elements. Order is not meaningful:

hcl
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:

hcl
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:

bash
cd ~/terraform-labs/terraform-data-types/list-vs-set && terraform plan -input=false -no-color
output
+ 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:

bash
cd ~/terraform-labs/terraform-data-types/list-vs-set && echo 'toset(["a","b","a"])' | terraform console
output
toset([
  "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:

hcl
variable "tags" {
  type = map(string)
  default = {
    env  = "dev"
    team = "platform"
  }
}

Look up a value by key:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'var.tags["env"]' | terraform console
output
"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:

bash
cd ~/terraform-labs/terraform-data-types/errors/list-for-set && terraform plan -input=false -no-color
output
Error: 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:

hcl
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:

bash
cd ~/terraform-labs/terraform-data-types/errors/tuple-mismatch && terraform validate
output
Error: 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:

hcl
variable "app" {
  type = object({
    name    = string
    enabled = bool
    ports   = list(number)
  })
  default = {
    name    = "web"
    enabled = true
    ports   = [80, 443]
  }
}

Access attributes with dot notation:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'var.app.name' | terraform console
output
"web"

Omitting a required attribute fails. From errors/malformed-object/:

bash
cd ~/terraform-labs/terraform-data-types/errors/malformed-object && terraform validate
output
Error: 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:

hcl
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:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'var.service.description' | terraform console
output
"default desc"

The optional labels map defaults to an empty map when omitted:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'var.service.labels' | terraform console
output
tomap({})

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:

hcl
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:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'null' | terraform console
output
null

Automatic 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:

bash
cd ~/terraform-labs/terraform-data-types/conversions && echo 'tostring(42)' | terraform console
output
"42"

Numeric strings parse back to numbers with tonumber:

bash
cd ~/terraform-labs/terraform-data-types/conversions && echo 'tonumber("42")' | terraform console
output
42

Not every string converts. tonumber("not-a-number") fails in console:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'tonumber("not-a-number")' | terraform console
output
Error: 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.

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'type(var.subnet_ids)' | terraform console
output
list(string)

Sets report their element type separately from lists:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'type(var.unique_zones)' | terraform console
output
set(string)

Object types list every attribute and its constraint:

bash
cd ~/terraform-labs/terraform-data-types/main && echo 'type(var.app)' | terraform console
output
object({
    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:

bash
cd ~/terraform-labs/terraform-data-types/errors/invalid-conversion && terraform validate
output
Error: 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


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.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)