Study guide
7.3 Complex Types: list, map, object, tuple, set
HashiCorp Certified: Terraform Associate 004
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan
2 Infrastructure as Code with Terraform
2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows
3 Terraform Fundamentals: Providers & State
3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State
4 Core Terraform Workflow: Init & Validate
4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration
5 Plan, Apply, Destroy, and Formatting
5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments
6 Resources, Data Sources, and References
6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns
7 Variables, Outputs, and Complex Types
7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set
8 Dynamic Configuration and Dependencies
8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules
9 Conditions, Secrets, and Sensitive Data
9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments
10 Terraform Modules
10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions
11 Terraform State Management
11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations
12 Maintain Infrastructure with Terraform
12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics
13 HCP Terraform
13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations
7.3 Complex Types: list, map, object, tuple, set
Key Takeaways Explain This Topic AI
Primitives are string, number, and bool; collections are list, set, and map of one element type; structural types are object and tuple with a schema of potentially different types
Index a list or tuple with [0]; index a map with ["key"] or .key; sets are unordered unique collections and cannot be indexed until you convert them
Put type constraints on variables so Terraform converts compatible values or raises a type-mismatch error before plan finishes
tolist, tomap, and toset normalize values — toset drops duplicates and order; converting a set of strings to a list sorts them lexicographically
list(object({ ... })) is the usual constraint for a typed list of records such as port mappings or subnet definitions
Last updated: August 2026
7.3 Complex Types: list, map, object, tuple, set
Quick Answer: string , number , and bool are primitives. list , set , and map are collections of one element type. object and tuple are structural types with a schema . Write list[0] and map["key"] . A set is unordered and unique — you cannot index it. Variable type constraints convert compatible values or error. Use tolist , tomap , and toset to normalize. The exam workhorse is list(object({ ... })) .
Objective 4d on Terraform Associate (004) is: understand and use complex types. Official pages: Types and Values and Type Constraints . Type constraints in Terraform 1.12 are valid on the type argument of an input variable .
Three layers of types
Kind Types Rule
Primitive string , number , bool One value. Not built from other types.
Collection list(T) , set(T) , map(T) Many values of one element type T .
Structural object({ ... }) , tuple([ ... ]) Many values that may have different types, described by a schema.
Special null Absence. Setting a resource argument to null is the same as omitting it.
list without an argument is shorthand for list(any) . Same for map . New configuration should write the full constructor: list(string) , map(number) .
There is also any , a placeholder, not a type. HashiCorp's warning is blunt: do not use any just to skip a constraint. The only honest use is passing an opaque blob straight into something like jsonencode without reading fields.
Collections versus structural types
list(T) — ordered sequence, indexes start at 0 . Duplicates allowed. var.azs[0] is the first availability zone.
set(T) — unique values, no order , no index . You cannot write var.azs[0] when azs is a set(string) . Convert with tolist first. Sets are the natural type for for_each over a collection of unique names.
map(T) — values keyed by a string . All values share type T . var.tags["Environment"] or var.tags.Environment if the key is a valid identifier. Prefer brackets when keys come from users.
object({ attr = TYPE, ... }) — named attributes, each with its own type. A matching value must include every required attribute. Extra attributes are discarded during conversion (lossy if you later convert back to a map).
tuple([TYPE, TYPE, ...]) — fixed length, each position has its own type. tuple([string, number, bool]) matches ["a", 15, true] and rejects a four-element list.
HCL literals look like lists and maps even when the physical type is a tuple or object. ["us-west-1a", "us-west-1c"] is a tuple of strings until a constraint converts it to list(string) . { name = "web", count = 3 } is an object until a map(...) constraint converts it — and that conversion fails if the values cannot share one type. variable "azs" {
- type = list(string)
- default = ["us-west-2a", "us-west-2b"]
- }
- variable "tags" {
- type = map(string)
- default = {
- Project = "billing"
- Environment = "prod"
- }
- }
- variable "image" {
- type = object({
- id = string
- arch = string
- hvm = bool
- })
- }
Indexing and attributes:
Value Access Notes
list / tuple var.azs[0] Whole number starting at zero.
map / object var.tags["Environment"] or var.image.id Bracket form is safer for arbitrary keys.
set (none) Convert: tolist(var.az_set)[0] . String sets become lexicographically ordered lists. Other element types have no guaranteed order.
Type constraints, conversion, and mismatch errors
Terraform converts automatically when the kinds are similar:
number / bool ↔ string when the string is a valid representation ( 15 ↔ "15" , true ↔ "true" ).
list ↔ tuple when the length works (a list converts to a tuple only if it has exactly the required number of elements).
map ↔ object when the map has at least the object's keys; extras are dropped.
list/tuple → set drops duplicates and order. set → list of strings sorts lexicographically.
Automatic conversion does not run for the equality operator. 1 == "1" is false.
A type-mismatch error means no conversion path exists. Official example: a map(string) cannot accept { name = ["Kristy", "Claudia"], age = 12 } because a tuple cannot become a string. variable "names" {
- type = list(string)
- }
# Assigned ["a", 15, true] becomes ["a", "15", "true"] — conversion succeeds.
- variable "labels" {
- type = map(string)
- }
# Assigned { name = ["a", "b"], age = 12 } — type mismatch, plan fails.
Write the constraint on the variable , not as a hope that callers will guess. default must itself convert to type .
tolist , tomap , toset
Explicit conversion is rarely required because Terraform converts at assignment. HashiCorp says to use these functions mainly to normalize module outputs . On 004 they also appear when you must index a set or force uniqueness.
Function Does Exam detail
tolist(x) Convert to a list From a set: order is undefined for a given type except that string sets become lexicographic lists, stable within one run. Mixed elements become the most general type ( tolist(["a", "b", 3]) → "3" as a string).
tomap(x) Convert to a map All values must share one type after conversion. tomap({ a = "foo", b = true }) becomes { a = "foo", b = "true" } .
toset(x) Convert to a set Drops duplicates and order. toset(["c", "b", "b"]) is { "b", "c" } . Mixed types generalize the same way. variable "example_set" {
- type = set(string)
- default = ["foo", "bar"]
- }
- locals {
- example_list = tolist(var.example_set)
- }
- output "first_element" {
- value = local.example_list[0]
- }
Do not write var.example_set[0] . That is a 4d fail.
When to use each type
string / number / bool — one scalar. Region, count, a feature flag.
list — ordered, positional, duplicates allowed. AZ preference order, a sequence you will index.
set — membership only. Unique names for for_each . You do not care about position.
map — same-typed values keyed by string. Tags, a lookup table of instance types.
object — one record with named fields of mixed types. A single image descriptor.
tuple — a fixed positional record ( [name, port, enabled] ). Rare in new modules; prefer an object so callers use names instead of positions.
list(object({...})) — many records. Subnets, listener rules, container ports.
- Worked example: list(object({...}))
- HashiCorp's own variable block example is the one to recognize on sight: variable "docker_ports" {
- type = list(object({
- internal = number
- external = number
- protocol = string
- }))
- description = "Port mappings for the container."
- default = [
- {
- internal = 8300
- external = 8300
- protocol = "tcp"
- }
- ]
- }
- resource "example_container" "app" {
- # each object is one mapping
- dynamic "ports" {
- for_each = var.docker_ports
- content {
- internal = ports.value.internal
- external = ports.value.external
- protocol = ports.value.protocol
- }
- }
- }
Each element must supply internal , external , and protocol with those types. An extra key on an assigned object is dropped during conversion. Missing protocol is a type error. var.docker_ports[0].external is 8300 . If you had used list(map(string)) you would lose number types and you could not require those three keys.
- A sibling pattern on 004 is a list of subnet objects: variable "private_subnets" {
- type = list(object({
- cidr = string
- az = string
- tags = map(string)
- }))
- }
That constraint rejects a bare list of CIDR strings and rejects an object that forgot az . That is the point of 4d: the type is the module's contract.
004 traps for complex types
list is ordered and indexable. set is neither.
map values share one type. object attributes can differ.
Extra object keys disappear on conversion; do not rely on them later.
tolist(set_of_strings) is sorted. tolist(set_of_objects) is not a stable business order.
any is not a shortcut for "I will figure it out."
Equality does not convert: true == "true" is false.