WintelGuy.com

Terraform Value Types Tutorial

Contents

Introduction

Terraform uses a powerful and flexible configuration language (HCL - HashiCorp Configuration Language) to define infrastructure as code. As with any programming or declarative language, understanding the types of values you're working with is fundamental to writing effective and error-free code.

In Terraform, value types are used to define variables, locals, module inputs, and more. Types determine how values behave, how they can be accessed or transformed, and whether they are valid for a given resource or input block.

Terraform supports a rich type system that includes:

  • Primitive types - the basic building elements: string, number, and bool
  • Collection types - groups of multiple values: list, set, and map
  • Structural types - organized related values: object and tuple
  • Dynamic and special types - any and null, used for flexible or optional values

This tutorial explores each of these types in detail, explains how and when to use them, and demonstrates techniques for converting between types using built-in functions. We'll also compare similar types (like list vs set, or map vs object) to help you choose the right one for your use case.

Whether you're writing simple configurations or building reusable modules, mastering Terraform's type system will make your code more predictable, modular, and robust.

Back to Top

Primitive Types

Terraform supports three primitive types: string, number, and bool. These are the simplest types used to represent individual values and are commonly used in variables, locals, and resource arguments.

string

A string is a sequence of Unicode characters enclosed in double quotes ("..."). Multiline strings can be created using heredoc syntax (<<EOT ... EOT).

Example:

variable "region" { type = string default = "us-central1" } # Standard Heredoc variable "user_data" { type = string default = <<EOT #!/bin/bash echo "Hello, World!" > /var/www/html/index.html EOT } # Indented Heredoc variable "startup_script" { type = string default = <<-EOT #!/bin/bash echo "Bootstrapping..." apt update apt install -y nginx EOT }

<<EOT (Standard Heredoc) preserves all whitespace exactly as written, including any leading tabs or spaces at the beginning of lines.

<<-EOT (Indented Heredoc) strips leading tabs (but not spaces) from each line in the heredoc content. This allows you to indent your heredoc content in your Terraform file for readability, without those indentations being preserved in the actual string value.

Terraform also supports string interpolation using the ${...} syntax, which allows you to embed variable values and expressions inside strings.

variable "env" { type = string default = "prod" } output "instance_name" { value = "web-${var.env}" }

Back to Top

number

A number represents a numeric value. It can be an integer or a floating-point number. Terraform does not distinguish between integer and float types internally; both are treated as number.

Example:

variable "instance_count" { type = number default = 3 }

You can use arithmetic operations and functions when defining numeric variables:

locals { double_count = var.instance_count * 2 } output "instance_count_message" { value = "Creating ${var.instance_count * 2} virtual machines" }

Back to Top

bool

A bool is a boolean value that can be either true or false. Booleans are commonly used in conditional expressions and to toggle features in configuration.

Example:

variable "enable_monitoring" { type = bool default = true }

You can use bool values directly in condition evaluations:

resource "google_monitoring_alert_policy" "cpu_alert" { count = var.enable_monitoring ? 1 : 0 # ... }

Back to Top

Structural and Collection Types

Terraform supports complex data types that allow you to group multiple values into structured collections.

Complex types fall into two main categories:

  • Collection types: list, set, and map containing multiple values of the same type
  • Structural types: object and tuple consisting of multiple values of several potentially distinct types

These types form the foundation for more advanced Terraform features like for_each, dynamic blocks, and complex variable schemas. Choosing the right type, especially when working with inputs and modules, helps make your configuration more predictable, modular, and reusable.

Let's review each type in more details.

list(type)

A list is an ordered collection of values, all of the same type. List elements are identified by a number (index), starting from 0.

Example:

variable "regions" { type = list(string) default = ["us-west1", "us-central1", "us-east1"] }

You can access individual list elements using the square-bracket index notation:

output "first_region" { value = var.regions[0] # "us-west1" }

Back to Top

map(type)

A map is a collection of key-value pairs, where the keys are strings and all values have the same type. Maps are great for structured configurations or lookup tables.

Example:

variable "prod_instance" { type = map(string) default = { name = "server-prod" size = "n2-standard-4" } }

You can use map's keys and either the square-bracket index notation or the dot-separated attribute notation to access map values:

output "instance_name" { value = var.prod_instance["name"] } output "instance_size" { value = var.prod_instance.size }

Back to Top

object({ ... })

An object has a fixed schema with named attributes, each with its own type declaration. Unlike a map, the types and structure of the values are explicitly defined. Objects are ideal for passing structured settings to modules and for creating predictable input formats

The schema for object types is a comma-separated series of <ATTR_NAME> = <TYPE>, pairs wrapped in curly braces - { <ATTR_NAME> = <TYPE>, <ATTR_NAME> = <TYPE>, ... }.

Example:

variable "app_config" { type = object({ name = string port = number enabled = bool }) default = { name = "myapp" port = 8080 enabled = true } }

Individual elements are accessed using the square-bracket index notation (var.app_config["name"]) or the dot-separated attribute notation (var.app_config.name).

Back to Top

set(type)

A set is an unordered collection of unique values, all of the same type. Sets do not preserve order, and duplicate values are automatically removed. Also, sets do not have any secondary identifiers, therefore you cannot directly access elements of a set by index.

Sets are useful when you need uniqueness and element order is not important, such as applying a tag list or creating service accounts.

Example:

variable "tags" { type = set(string) default = ["web", "production"] }

Back to Top

tuple([ ... ])

A tuple is an ordered collection of values, where each position can have a different type. Tuples are fixed-length and positional. Use tuples when the position and type of each element are significant.

The schema for tuple types is a comma-separated series of types wrapped in a pair of square brackets [ <TYPE>, <TYPE>, ... ].

Example:

variable "app_info" { type = tuple([string, number, bool]) default = ["myapp", 8080, true] }

Items are accessed using the square-bracket index notation (var.app_info[0]).

Back to Top

Dynamic and Special Types

In addition to primitive and structural types, Terraform supports special types - any and null that provide flexibility when writing reusable modules or dynamic logic.

any

The any type acts as a wildcard type that accepts any valid Terraform value, whether primitive, collection, or structural.

It's often used in situations when you want to allow maximum flexibility and let the user supply any kind of value, such as:

  • Where value types is not impacting functionality
  • When you're not sure what type the input will be and want to simplify the development process
  • You want to disable type checking (Note: This makes configuration prone for runtime errors and harder to validate)

Use any sparingly, and only when truly needed. Prefer more specific types whenever possible or use object type constraints with optional modifier.

Examples:

variable "config" { type = any # Valid values could be: # string: "abc" # list: ["a", "b"] # map: { name = "test" } # object: { port = 8080, secure = true } } # Using "optional" modifier variable "config_optional" { type = object({ name = string # required attribute desc = optional(string) # optional attribute size = optional(number, 3) # optional attribute with default value }) }

Back to Top

null

The null value in Terraform represents the absence or omission of a value. It's not a type itself, but a special value that can be assigned to any type (except bool, where it defaults to false).

Terraform utilizes null value in several ways:

  • To remove or unset an argument of a resource
  • To defer to default values or raise an error if the argument is mandatory
  • To conditionally skip resource arguments

Back to Top

Nested and Complex Object Types

Terraform's type system supports nesting of value types, allowing you to define rich, structured data. These nested types are particularly useful for defining complex input variables and local values in reusable modules.

You can combine collection types (list, set, map) and structural types (object, tuple) to model advanced data structures such as:

  • list of maps
  • map of objects
  • map of tuples
  • object with nested lists or maps, etc.

Examples 1: List of Maps

A list of maps is commonly used to represent a series of items where each item has the same schema (like tags, policies, rules, etc.).

variable "firewall_rules" { type = list(map(string)) default = [ { name = "allow-ssh" port = "22" }, { name = "allow-http" port = "80" } ] }

Examples 2: Map of Tuples

A map of tuples is used when each key maps to a fixed set of values of different types.

variable "dns_zones" { type = map(tuple([string, bool])) default = { "zone1" = ["zone1.example.com", true] "zone2" = ["zone2.example.com", false] } }

Examples 3: Object with Nested List

You can define an object that includes nested collections, such as a list of objects inside an object.

variable "app_config" { type = object({ name = string ports = list(number) labels = map(string) }) default = { name = "frontend" ports = [80, 443] labels = { env = "prod" tier = "web" } } }

Back to Top

Type Comparison

Terraform offers several types that can seem similar but behave differently. Understanding how they compare helps you choose the right one for clarity, validation, and functionality.

Here are the key similarities and differences between the similar value types:

map vs object

Similarities:

  • Both map and object are key-value collections
  • Keys/attributes are always strings
  • Elements are accessed using the square-bracket index notation or the dot-separated attribute notation - var.abc["name"] or var.abc.name

Differences:

 Feature  map(type) object({ ... })
Schema Arbitrary keys, all values of the same type Named attributes with specific types
Flexibility Dynamic, number of items may vary Fixed-length, attributes must match the schema exactly

Back to Top

tuple vs list

Similarities:

  • Both tuple and list are ordered collections
  • Items are accessed using the square-bracket index notation - var.abc[index]

Differences:

 Feature  tuple([type1, type2, ...]) list(type)
Value Types Each item can have a different type All items must have the same type
Flexibility Fixed-length, positional structure Dynamic, number of items may vary

Back to Top

list vs set

Similarities:

  • Both list and set are collections of values of the same type
  • Number of elements (length) may vary

Differences:

 Feature   list(type) set(type)
Ordering Preserves element order Unordered (order is ignored)
Duplicates Allowed Not allowed (duplicates removed during conversion)
Indexable Yes, access with [index] No, index access not supported

Back to Top

Comparison Summary

Type Ordered Key-Value Value Type(s)
list(type) Yes No Same type
map(type) No Yes Same type
object({...}) No Yes Named fields, allows different types
set(type) No No Same type
tuple([...]) Yes No Positional, allows different types

Terraform Types Venn Diagram:

Back to Top

Type Conversion

Terraform allows both implicit and explicit value type conversions, and offers built-in functions to convert between types when needed.

Understanding how Terraform handles type conversion helps prevent errors and makes your configurations more predictable when working with functions or module inputs.

Let's explore implicit and explicit type conversions in more details.

Implicit Type Conversion

Terraform performs automatic (implicit) type conversion in situations where it is safe and unambiguous. These conversions typically occur when passing values to variables, modules, or resource arguments that expect a different but compatible type.

Conversion of Primitive Types

Terraform will automatically convert between primitive types when possible:

  • String to number or boolean, if the string contains a valid representation:
    "42" → 42
    "true" → true
  • Number to string, in a string context:
    "Count: ${3}" → "Count: 3"
  • Boolean to string, in string interpolation:
    "Enabled: ${true}" → "Enabled: true"

Conversion of Complex Types

Terraform also performs automatic conversions between similar collection and structural types when their content and structure are compatible. The actual conversion behavior is determined by the source and target types and the structure and content of the value being assigned. In some scenarios, conversion may result in data loss.

Also remember, when assigning a value to a variable, Terraform interprets input as follows:

  • Square brackets [ ... ] create a tuple
  • Curly braces { ... } with named keys create an object

Below we provide more details about conversions between some Terraform value types.

Back to Top

map to object

A map(type) can be implicitly converted to an object({...}) if the map's keys match the attributes expected in the object and the values can be converted to the corresponding object's types.

Constraints:

  • All object's key-value pairs must be present in the map
  • Any extra key-value pairs present in the map are discarded during conversion
  • All map's values must be convertible to the declared object's attribute types

Example:

The following code assigns a map(string) value (variable mymap) to the module's input variable of object() type and displays the conversion results:

# main.tf variable "mymap" { type = map(string) default = { a = "b" b = "5" c = "extra" } } module "mirror" { source = "./mirror" input = var.mymap } output "input" { value = var.mymap } output "result" { value = module.mirror.result }
# mirror/mirror.tf variable "input" { type = object({ a = string b = number }) } output "result" { value = var.input }

To display the conversion results, run
terraform init
terraform apply -auto-approve

Outputs: input = tomap({ "a" = "b" "b" = "5" "c" = "extra" }) result = { "a" = "b" "b" = 5 }

Notice how Terraform converts a string value ("b" = "5") into a number ("b" = 5) and discards "c" = "extra".

You can also use Terraform console to check the source and target types:

$ terraform console > var.mymap tomap({ "a" = "b" "b" = "5" "c" = "extra" }) > type(var.mymap) map(string) > module.mirror.result { "a" = "b" "b" = 5 } > type(module.mirror.result) object({ a: string, b: number, }) >

Conversion fails if the source map() does not contains all keys present in the target object():

$ terraform plan -var 'mymap={a="text", c="true"}' ╷ │ Error: Invalid value for input variable │ │ on main.tf line 14, in module "mirror": │ 14: input = var.mymap │ │ The given value is not suitable for module.mirror.var.input declared at mirror/mirror.tf:3,1-17: map has no element for required attribute "b".

Back to Top

object to map

An object() can be implicitly converted to a map(type) if all attribute values can be converted to the target value type.

Constraints:

  • All attribute values must be convertible to the specified map's type

Example:

The following code assigns a object() value (variable myobj) to the module's input variable of map(string) type and displays the conversion results:

# main.tf variable "myobj" { type = object({ a = string b = number c = bool }) default = { a = "b", b = 5, c = true } } module "mirror" { source = "./mirror" input = var.myobj } output "input" { value = var.myobj } output "result" { value = module.mirror.result }
# mirror/mirror.tf variable "input" { type = map(string) } output "result" { value = var.input }

To display the conversion results, run
terraform init
terraform apply -auto-approve

Outputs: input = { "a" = "b" "b" = 5 "c" = true } result = tomap({ "a" = "b" "b" = "5" "c" = "true" })

Notice how Terraform converts the numeric and boolean values into strings.

You can also use Terraform console to check the source and target types:

$ terraform console > var.myobj { "a" = "b" "b" = 5 "c" = true } > type(var.myobj) object({ a: string, b: number, c: bool, }) > module.mirror.result tomap({ "a" = "b" "b" = "5" "c" = "true" }) > type(module.mirror.result) map(string) >

Back to Top

list to set

A list(type) can be converted to a set(type) if all element values can be converted to the target set's type.

Constraints:

  • All list's values must be convertible to the declared set's type
  • Duplicate items are removed
  • Ordering is lost

Example:

The following code assigns a list(number) value (variable mylist) to the module's input variable of set(string) type and displays the conversion results:

# main.tf variable "mylist" { type = list(number) default = [3, 2, 1, 2] } module "mirror" { source = "./mirror" input = var.mylist } output "input" { value = var.mylist } output "result" { value = module.mirror.result }
# mirror/mirror.tf variable "input" { type = set(string) } output "result" { value = var.input }

To display the conversion results, run
terraform init
terraform apply -auto-approve

Outputs: input = tolist([ 3, 2, 1, 2, ]) result = toset([ "1", "2", "3", ])

Notice how Terraform converts all numeric values into strings and removes duplicates.

You can also use the type() function to check the source and target types in the Terraform console (as shown in the previous example).

Back to Top

set to list

A set(type) can be converted to a list(type) if all element values can be converted to the target type; element order is not guaranteed.

Constraints:

  • All values must be convertible to the declared set's type.
  • Ordering is lost (resulting list may appear in arbitrary order)

Example:

The following code assigns a set(number) value (variable myset) to the module's input variable of list(string) type and displays the conversion results:

# main.tf variable "myset" { type = set(number) default = [3, 2, 1] } module "mirror" { source = "./mirror" input = var.myset } output "input" { value = var.myset } output "result" { value = module.mirror.result }
# mirror/mirror.tf variable "input" { type = list(string) } output "result" { value = var.input }

To display the conversion results, run
terraform init
terraform apply -auto-approve

Outputs: input = toset([ 1, 2, 3, ]) result = tolist([ "1", "2", "3", ])

Notice how Terraform converts all numeric values into strings.

You can also use the type() function to check the source and target types in the Terraform console (as shown in one of the previous examples).

Back to Top

tuple to list

A tuple([...]) can be converted to a list(type) if all elements are of the same type or convertible to the list's element type.

Constraints:

  • All values must be convertible to the declared list's type

Example:

The following code assigns a tuple([...]) value (variable mytuple) to the module's input variable of list(string) type and displays the conversion results:

# main.tf variable "mytuple" { type = tuple([string, number, bool]) default = ["text", 2, true] } module "mirror" { source = "./mirror" input = var.mytuple } output "input" { value = var.mytuple } output "result" { value = module.mirror.result }
# mirror/mirror.tf variable "input" { type = list(string) } output "result" { value = var.input }

To display the conversion results, run
terraform init
terraform apply -auto-approve

Outputs: input = [ "text", 2, true, ] result = tolist([ "text", "2", "true", ])

Notice how Terraform converts all numeric and boolean values into strings.

You can also use the type() function to check the source and target types in the Terraform console (as shown in one of the previous examples).

Back to Top

tuple to set

A tuple([...]) can be converted to a set(type) if all elements are of the same type or can be converted to the set's type.

Constraints:

  • All values must be convertible to the declared set's type
  • Duplicates are removed.
  • Ordering is lost

Example:

The following code assigns a tuple([...]) value (variable mytuple) to the module's input variable of set(string) type and displays the conversion results:

# main.tf variable "mytuple" { type = tuple([string, number, number, bool]) default = ["text", 2, 2, true] } module "mirror" { source = "./mirror" input = var.mytuple } output "input" { value = var.mytuple } output "result" { value = module.mirror.result }
# mirror/mirror.tf variable "input" { type = set(string) } output "result" { value = var.input }

To display the conversion results, run
terraform init
terraform apply -auto-approve

Outputs: input = [ "text", 2, 2, true, ] result = toset([ "2", "text", "true", ])

Notice how Terraform converts all numeric and boolean values into strings and removes duplicates

You can also use the type() function to check the source and target types in the Terraform console (as shown in one of the previous examples).

Back to Top

Explicit Type Conversion

Terraform provides a set of built-in functions to convert between value types:

  Function      Description Example
tobool() Converts strings to boolean tobool("true") → true
tolist() Converts a set or tuple to a list tolist(["a", 5, true]) → ["a", "5", "true"]
tomap() Converts a structure to a map tomap({a = "b", b = 5}) → {a = "b", b = "5"}
tonumber() Converts a string (if numeric) to a number tonumber("4.200") → 4.2
toset() Converts a list or tuple to a set toset(["a", "a", "b"]) → ["a", "b"]
tostring() Converts any primitive or structured value to a string tostring(4.200) → "4.2"
type() Returns the type of a value (Terraform console only) type(["a", 5, true]) →
 tuple([
   string,
   number,
   bool,
 ])

Example: Using conversion functions in terraform console

In an empty directory create a file, for example: main.tf, containing the following code:

# main.tf variable "myset" { type = set(string) default = ["a", "5", "true"] } variable "mytuple" { type = tuple([string, number, bool]) default = ["a", 5, true] } variable "mylist" { type = list(string) default = ["a", "a", "b"] } variable "myobj" { type = object({ a = string b = number }) default = { a = "b", b = 5 } }

Launch terraform console and try some of the conversion examples from the table above.

$ terraform console > tobool("true") true > tolist(var.mytuple) tolist([ "a", "5", "true", ]) > tomap(var.myobj) tomap({ "a" = "b" "b" = "5" }) > tonumber("4.200") 4.2 > toset(var.mylist) toset([ "a", "b", ]) > tostring(4.200) "4.2" > type(var.mytuple) tuple([ string, number, bool, ]) >

Terraform's type system is flexible enough to express rich and structured inputs. Understanding how type conversion works, especially between similar types, will help you build reusable modules, prevent subtle bugs, and write more reliable configuration code.

Back to Top

Conclusion and Best Practices

Terraform's wide selection of value types offers both flexibility and precision for managing infrastructure as code. In this tutorial we analyzed how primitive, collection, and structural types work and illustrated how Terraform performs implicit and explicit type conversions.

By understanding the nuances of the Terraform type system and following the best practices outlined below, you'll improve reliability and reusability of your infrastructure code and reduce bugs caused by unexpected behaviors or silent type mismatches.

Best Practices for Using Terraform Types:

  • Explicitly define variable types. Use type = ... in variable blocks to clearly document what structure and value types are expected. This simplifies validation and improves code maintainability.
  • Prefer structural types (object, tuple) for strict schema enforcement. Use object({...}) when you need clearly defined named attributes, and tuple([...]) for fixed-position mixed-type arrays.
  • Use dynamic types (any, list(any), etc.) sparingly. These types can be helpful during prototyping, but should be replaced with more specific types to avoid runtime errors and unexpected conversions.
  • Avoid relying on implicit conversion in variable defaults. Use conversion functions or locals if you need to perform type conversions.
  • Use type() in Terraform console to inspect values. Helpful when debugging complex or nested input types.
  • Convert explicitly when dealing with dynamic or mixed content. Use functions like tostring(), tolist(), tomap(), etc. to ensure predictable code behavior.
  • Use sets only when ordering does not matter. Remember that a set is unordered, don't use it when sequence of items is important.
  • Validate input lengths for tuples or fixed-size structures. Mismatches in length between source and target will cause errors.

Do you want to learn more about Terraform? Please, check our other tutorials:
Handling Terraform State in Multi-Environment Deployments
Understanding Terraform Variable Precedence
Terraform count Explained with Practical Examples
Terraform for_each Tutorial with Practical Examples
Exploring Terraform dynamic Blocks with GCP Examples
Working with External Data in Terraform
Handling Sensitive and Ephemeral Data in Terraform
Terraform Modules FAQ