In Terraform, what does the `type` argument in a `variable` block do, and what categories of type constraint can you declare?
answer
- three families of constraint
- primitive, collection, structural
- conversion first, then rejection
- checked while variables are evaluated
- omitting type means any
basics
~20 sTerraform'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 sThe `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 linesvariable "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
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.
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.
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.
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