skip to content

In Terraform, what does the `type` argument in a `variable` block do, and what categories of type constraint can you declare?

level: juniorimportance: must knowfreq 72%

answer

  1. three families of constraint
  2. primitive, collection, structural
  3. conversion first, then rejection
  4. checked while variables are evaluated
  5. omitting type means any

basics

~20 s

Terraform's type argument constrains what a caller may pass, rejecting mismatches during plan before anything is created. Type constraints come in three families: primitives (string, number, bool), collections (list, set, map), and structural types (object, tuple).

solid answer

~50 s

The `type` argument on a `variable` block is a type constraint — a declaration of the shape of value the module accepts. There are three families. Primitives are `string`, `number` and `bool`. Collections — `list(T)`, `set(T)`, `map(T)` — hold any number of elements that must all share one element type. Structural types — `object({name = string, size = number})` and `tuple([string, number])` — describe a fixed shape where each attribute or position has its own type. There is also `any`, a wildcard, which is what you get if you omit `type` entirely. Terraform checks the supplied value against the constraint while evaluating variables at the start of a plan, converting where the conversion is unambiguous (the string `"5"` to the number `5`, a `["a","b"]` literal to `list(string)`) and failing the whole run where it is not. The payoff is that a bad input is caught in seconds rather than halfway through an apply.

code

hcl · 19 lines
hcl
variable "region" {
  type = string
}

variable "azs" {
  type = list(string)
}

variable "tags" {
  type = map(string)
}

variable "database" {
  type = object({
    engine   = string
    size_gb  = number
    multi_az = bool
  })
}

go deeper

for a junior

Be ready to name the three families and write a variable block with type, description and default. Say plainly that a wrong-shaped value fails the plan rather than the apply.

for a middle

Explain the conversion step: a bracketed literal is a tuple that Terraform converts to a list or set, and "5" converts to a number. Be able to nest a structural type inside a collection and justify it.

for a senior

Show why a strict interface is operationally cheap: a mis-shaped input caught during variable evaluation costs a terminal message, while the same mistake reaching apply leaves a partially built estate to unwind.

for a principal

Own the interface-stability angle: a module's type constraints are its contract with every consumer, so tightening one is a breaking change that belongs behind a version bump and a documented migration.

## What a type constraint actually is A `variable` block declares one input to a Terraform module — the root module included. Its `type` argument is not a runtime check bolted on the side; it is part of the module's published interface, and Terraform uses it to *convert* as well as to *reject*. ```hcl variable "instance_name" { type = string description = "Name tag applied to the instance" } ``` When Terraform evaluates the root module's variables at the beginning of a plan, it takes the value supplied for `instance_name`, attempts to convert it to `string`, and either succeeds (the module now sees a `string`) or aborts the entire run with an "Invalid value for input variable" error. For a child module, the same thing happens to each argument written in the calling `module` block. Nothing is created, nothing is refreshed against the API — the failure lands before the graph walk begins. ## The three families **Primitive types** are `string`, `number` and `bool`. Terraform has exactly one number type; there is no separate integer. **Collection types** — `list(T)`, `set(T)` and `map(T)` — hold zero or more elements that must all have the same type `T`. `list(string)` is ordered and indexed by position, `set(string)` is unordered and de-duplicated, `map(string)` is keyed by arbitrary strings. **Structural types** — `object({...})` and `tuple([...])` — describe a fixed shape whose members may each have a different type: ```hcl variable "database" { type = object({ engine = string size_gb = number multi_az = bool }) } ``` A caller passes `{ engine = "postgres", size_gb = 100, multi_az = true }`. Each attribute is checked against its own declared type. `tuple([string, number])` is the positional equivalent and is rare in hand-written code — it mostly shows up as the type Terraform infers for a mixed literal like `["a", 1]`. These nest freely: `list(object({name = string, port = number}))` is an ordinary and very useful constraint for something like a list of listener rules. ## Conversion comes before rejection Terraform is not strictly typed in the "exact match or die" sense. It applies automatic conversions where there is only one sensible reading: `"5"` converts to the number `5` for a `number` variable, `true` converts to `"true"` for a `string`, and a bracketed literal — which is really a *tuple* — converts to `list(string)` or `set(string)` when its elements can all become strings. A braced literal is an *object* and converts to `map(...)` on the same terms. Conversions that would lose information or are ambiguous, such as `"abc"` to `number`, fail. This is why a `.tfvars` file full of quoted values usually works against numeric variables: the conversion, not the author, did the work. ## Omitting the type If you leave `type` off, the variable's constraint is `any` and every value is accepted as-is. That is legal, and it is what a lot of older module code does, but it means the module has no declared interface: a caller learns the expected shape only by reading the module body or by watching the plan fail somewhere deep inside a resource argument. ## Why this matters more in infrastructure code than elsewhere Infrastructure code has an unusually expensive failure mode: a run that dies in the middle has already created some real resources and left the rest undone. Type constraints are the cheapest possible test in that world — they run in the first second of `terraform plan`, cost nothing, and need no credentials or test harness. Declaring `number` on a retention period and `object({...})` on a subnet definition converts a class of caller mistakes from a partially-applied estate into a message on the terminal. The practical rule for module authors: declare the tightest type that is honestly true. `string` beats `any`; `list(object({...}))` beats `list(any)`; and where the shape is genuinely rich but partly optional, the object type with optional attributes is the modern way to say so.

  • Where in the Terraform run does a type mismatch surface, and why does that timing matter?
    While Terraform evaluates variables at the start of the plan — for the root module from the supplied values, for a child module from the arguments in its `module` block. The run aborts before the graph is walked, so no resource has been created or changed. That is the difference between a terminal error and a half-applied estate you now have to reconcile.
  • What is the difference between a tuple and a list in Terraform, and when do you actually meet a tuple?
    A `list(T)` holds any number of elements that all share one type; a `tuple([...])` has a fixed length with a declared type per position. You rarely declare tuples, but you meet them constantly: every bracketed literal in HCL is a tuple, which Terraform then converts to the list or set the constraint asks for. A mixed literal like `["a", 1]` stays a tuple because no single element type fits.
  • Is a type constraint enough to guarantee a variable's value is usable?
    No. A constraint describes shape, not meaning: `number` accepts `-1` for a retention period, and `string` accepts `"eu-nowhere-1"` as a region. Types stop mis-shaped input; they say nothing about whether a well-shaped value is sensible. That is a separate concern from the type system.

saying these in an interview costs you the question

  • Thinking type constraints are checked only at apply time
  • Claiming Terraform has separate int and float types
  • Believing collections can hold mixed element types
  • Assuming omitting type makes the variable required
  • Treating object and map as interchangeable

context