- Expressions
- Type Constraints
Type Constraints
OpenTF module authors and provider developers can use detailed type constraints to validate user-provided values for their input variables and resource arguments. This requires some additional knowledge about OpenTF's type system, but allows you to build a more resilient user interface for your modules and resources.
Type Keywords and Constructors
Type constraints are expressed using a mixture of type keywords and function-like constructs called type constructors.
- Type keywords are unquoted symbols that represent a static type.
- Type constructors are unquoted symbols followed by a pair of parentheses, which contain an argument that specifies more information about the type. Without its argument, a type constructor does not fully represent a type; instead, it represents a kind of similar types.
Type constraints look like other kinds of OpenTF
expressions, but are a special syntax. Within the
OpenTF language, they are only valid in the type
argument of an
input variable.
Primitive Types
A primitive type is a simple type that isn't made from any other types. All primitive types in OpenTF are represented by a type keyword. The available primitive types are:
string
: a sequence of Unicode characters representing some text, such as"hello"
.number
: a numeric value. Thenumber
type can represent both whole numbers like15
and fractional values such as6.283185
.bool
: eithertrue
orfalse
.bool
values can be used in conditional logic.
Conversion of Primitive Types
The OpenTF language will automatically convert number
and bool
values
to string
values when needed, and vice-versa as long as the string contains
a valid representation of a number or boolean value.
true
converts to"true"
, and vice-versafalse
converts to"false"
, and vice-versa15
converts to"15"
, and vice-versa
Complex Types
A complex type is a type that groups multiple values into a single value. Complex types are represented by type constructors, but several of them also have shorthand keyword versions.
There are two categories of complex types: collection types (for grouping similar values), and structural types (for grouping potentially dissimilar values).
Collection Types
A collection type allows multiple values of one other type to be grouped together as a single value. The type of value within a collection is called its element type. All collection types must have an element type, which is provided as the argument to their constructor.
For example, the type list(string)
means "list of strings", which is a
different type than list(number)
, a list of numbers. All elements of a
collection must always be of the same type.
The three kinds of collection type in the OpenTF language are:
list(...)
: a sequence of values identified by consecutive whole numbers starting with zero.The keyword
list
is a shorthand forlist(any)
, which accepts any element type as long as every element is the same type. This is for compatibility with older configurations; for new code, we recommend using the full form.map(...)
: a collection of values where each is identified by a string label.The keyword
map
is a shorthand formap(any)
, which accepts any element type as long as every element is the same type. This is for compatibility with older configurations; for new code, we recommend using the full form.Maps can be made with braces ({}) and colons (:) or equals signs (=): { "foo": "bar", "bar": "baz" } OR { foo = "bar", bar = "baz" }. Quotes may be omitted on keys, unless the key starts with a number, in which case quotes are required. Commas are required between key/value pairs for single line maps. A newline between key/value pairs is sufficient in multi-line maps.
NoteAlthough colons are valid delimiters between keys and values,
opentf fmt
ignores them. In contrast,opentf fmt
attempts to vertically align equals signs.set(...)
: a collection of unique values that do not have any secondary identifiers or ordering.
Structural Types
A structural type allows multiple values of several distinct types to be grouped together as a single value. Structural types require a schema as an argument, to specify which types are allowed for which elements.
The two kinds of structural type in the OpenTF language are:
object(...)
: a collection of named attributes that each have their own type.The schema for object types is
{ <KEY> = <TYPE>, <KEY> = <TYPE>, ... }
— a pair of curly braces containing a comma-separated series of<KEY> = <TYPE>
pairs. Values that match the object type must contain all of the specified keys, and the value for each key must match its specified type. (Values with additional keys can still match an object type, but the extra attributes are discarded during type conversion.)tuple(...)
: a sequence of elements identified by consecutive whole numbers starting with zero, where each element has its own type.The schema for tuple types is
[<TYPE>, <TYPE>, ...]
— a pair of square brackets containing a comma-separated series of types. Values that match the tuple type must have exactly the same number of elements (no more and no fewer), and the value in each position must match the specified type for that position.
For example: an object type of object({ name=string, age=number })
would match
a value like the following:
{
name = "John"
age = 52
}
Also, an object type of object({ id=string, cidr_block=string })
would match
the object produced by a reference to an aws_vpc
resource, like
aws_vpc.example_vpc
; although the resource has additional attributes, they
would be discarded during type conversion.
Finally, a tuple type of tuple([string, number, bool])
would match a value
like the following:
["a", 15, true]
Complex Type Literals
The OpenTF language has literal expressions for creating tuple and object values, which are described in Expressions: Literal Expressions as "list/tuple" literals and "map/object" literals, respectively.
OpenTF does not provide any way to directly represent lists, maps, or sets. However, due to the automatic conversion of complex types (described below), the difference between similar complex types is almost never relevant to a normal user, and most of the OpenTF documentation conflates lists with tuples and maps with objects. The distinctions are only useful when restricting input values for a module or resource.
Conversion of Complex Types
Similar kinds of complex types (list/tuple/set and map/object) can usually be used interchangeably within the OpenTF language, and most of OpenTF's documentation glosses over the differences between the kinds of complex type. This is due to two conversion behaviors:
- Whenever possible, OpenTF converts values between similar kinds of complex
types if the provided value is not the exact type requested. "Similar kinds"
is defined as follows:
- Objects and maps are similar.
- A map (or a larger object) can be converted to an object if it has at least the keys required by the object schema. Any additional attributes are discarded during conversion, which means map -> object -> map conversions can be lossy.
- Tuples and lists are similar.
- A list can only be converted to a tuple if it has exactly the required number of elements.
- Sets are almost similar to both tuples and lists:
- When a list or tuple is converted to a set, duplicate values are discarded and the ordering of elements is lost.
- When a
set
is converted to a list or tuple, the elements will be in an arbitrary order. If the set's elements were strings, they will be in lexicographical order; sets of other element types do not guarantee any particular order of elements.
- Objects and maps are similar.
- Whenever possible, OpenTF converts element values within a complex type, either by converting complex-typed elements recursively or as described above in Conversion of Primitive Types.
For example: if a module argument requires a value of type list(string)
and a
user provides the tuple ["a", 15, true]
, OpenTF will internally transform
the value to ["a", "15", "true"]
by converting the elements to the required
string
element type. Later, if the module uses those elements to set different
resource arguments that require a string, a number, and a bool (respectively),
OpenTF will automatically convert the second and third strings back to the
required types at that time, since they contain valid representations of a
number and a bool.
On the other hand, automatic conversion will fail if the provided value
(including any of its element values) is incompatible with the required type. If
an argument requires a type of map(string)
and a user provides the object
{name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}
, OpenTF
will raise a type mismatch error, since a tuple cannot be converted to a string.
Dynamic Types: The "any" Constraint
any
is very rarely the correct type constraint to use.
Do not use any
just to avoid specifying a type constraint. Always write an
exact type constraint unless you are truly handling dynamic data.
The keyword any
is a special construct that serves as a placeholder for a
type yet to be decided. any
is not itself a type: when interpreting a
value against a type constraint containing any
, OpenTF will attempt to
find a single actual type that could replace the any
keyword to produce
a valid result.
The only situation where it's appropriate to use any
is if you will pass
the given value directly to some other system without directly accessing its
contents. For example, it's okay to use a variable of type any
if you use
it only with jsonencode
to pass the full value directly to a resource, as
shown in the following example:
variable "settings" {
type = any
}
resource "aws_s3_object" "example" {
# ...
# This is a reasonable use of "any" because this module
# just writes any given data to S3 as JSON, without
# inspecting it further or applying any constraints
# to its type or value.
content = jsonencode(var.settings)
}
If any part of your module accesses elements or attributes of the value, or
expects it to be a string or number, or any other non-opaque treatment, it
is incorrect to use any
. Write the exact type that your module is expecting
instead.
any
with Collection Types
All of the elements of a collection must have the same type, so if you use
any
as the placeholder for the element type of a collection then OpenTF
will attempt to find a single exact element type to use for the resulting
collection.
For example, given the type constraint list(any)
, OpenTF will examine
the given value and try to choose a replacement for the any
that would
make the result valid.
If the given value were ["a", "b", "c"]
-- whose physical type is
tuple([string, string, string])
-- OpenTF analyzes this as follows:
- Tuple types and list types are similar per the previous section, so the tuple-to-list conversion rule applies.
- All of the elements in the tuple are strings, so the type constraint
string
would be valid for all of the list elements. - Therefore in this case the
any
argument is replaced withstring
, and the final concrete value type islist(string)
.
If the elements of the given tuple are not all of the same type then OpenTF will attempt to find a single type that they can all convert to. OpenTF will consider various conversion rules as described in earlier sections.
- If the given value were instead
["a", 1, "b"]
then OpenTF would still selectlist(string)
, because of the primitive type conversion rules, and the resulting value would be["a", "1", "b"]
due to the string conversion implied by that type constraint. - If the given value were instead
["a", [], "b"]
then the value cannot conform to the type constraint: there is no single type that both a string and an empty tuple can convert to. OpenTF would reject this value, complaining that all elements must have the same type.
Although the above examples use list(any)
, a similar principle applies to
map(any)
and set(any)
.
Optional Object Type Attributes
OpenTF typically returns an error when it does not receive a value for specified object attributes. When you mark an attribute as optional, OpenTF instead inserts a default value for the missing attribute. This allows the receiving module to describe an appropriate fallback behavior.
To mark attributes as optional, use the optional
modifier in the object type constraint. The following example creates optional attribute b
and optional attribute with a default value c
.
variable "with_optional_attribute" {
type = object({
a = string # a required attribute
b = optional(string) # an optional attribute
c = optional(number, 127) # an optional attribute with default value
})
}
The optional
modifier takes one or two arguments.
- Type: (Required) The first argument specifies the type of the attribute.
- Default: (Optional) The second argument defines the default value that OpenTF should use if the attribute is not present. This must be compatible with the attribute type. If not specified, OpenTF uses a
null
value of the appropriate type as the default.
An optional attribute with a non-null
default value is guaranteed to never have the value null
within the receiving module. OpenTF will substitute the default value both when a caller omits the attribute altogether and when a caller explicitly sets it to null
, thereby avoiding the need for additional checks to handle a possible null value.
OpenTF applies object attribute defaults top-down in nested variable types. This means that OpenTF applies the default value you specify in the optional
modifier first and then later applies any nested default values to that attribute.
Example: Nested Structures with Optional Attributes and Defaults
The following example defines a variable for storage buckets that host a website. This variable type uses several optional attributes, including website
, which is itself an optional object
type that has optional attributes and defaults.
variable "buckets" {
type = list(object({
name = string
enabled = optional(bool, true)
website = optional(object({
index_document = optional(string, "index.html")
error_document = optional(string, "error.html")
routing_rules = optional(string)
}), {})
}))
}
The following example terraform.tfvars
file specifies three bucket configurations for var.buckets
.
production
sets the routing rules to add a redirectarchived
uses default configuration, but is disableddocs
overrides the index and error documents to use text files
The production
bucket does not specify the index and error documents, and the archived
bucket omits the website configuration entirely. OpenTF will use the default values specified in the bucket
type constraint.
buckets = [
{
name = "production"
website = {
routing_rules = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]
EOT
}
},
{
name = "archived"
enabled = false
},
{
name = "docs"
website = {
index_document = "index.txt"
error_document = "error.txt"
}
},
]
This configuration produces the following variable values.
- For the
production
anddocs
buckets, OpenTF setsenabled
totrue
. OpenTF also supplies default values forwebsite
, and then the values specified indocs
override those defaults. - For the
archived
anddocs
buckets, OpenTF setsrouting_rules
to anull
value. When OpenTF does not receive optional attributes and there are no specified defaults, OpenTF populates those attributes with anull
value. - For the
archived
bucket, OpenTF populates thewebsite
attribute with the default values specified in thebuckets
type constraint.
tolist([
{
"enabled" = true
"name" = "production"
"website" = {
"error_document" = "error.html"
"index_document" = "index.html"
"routing_rules" = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]
EOT
}
},
{
"enabled" = false
"name" = "archived"
"website" = {
"error_document" = "error.html"
"index_document" = "index.html"
"routing_rules" = tostring(null)
}
},
{
"enabled" = true
"name" = "docs"
"website" = {
"error_document" = "error.txt"
"index_document" = "index.txt"
"routing_rules" = tostring(null)
}
},
])
Example: Conditionally setting an optional attribute
Sometimes the decision about whether or not to set a value for an optional argument needs to be made dynamically based on some other data. In that case, the calling module
block can use a conditional expression with null
as one of its result arms to represent dynamically leaving the argument unset.
With the variable "buckets"
declaration shown in the previous section, the following example conditionally overrides the index_document
and error_document
settings in the website
object based on a new variable var.legacy_filenames
:
variable "legacy_filenames" {
type = bool
default = false
nullable = false
}
module "buckets" {
source = "./modules/buckets"
buckets = [
{
name = "maybe_legacy"
website = {
error_document = var.legacy_filenames ? "ERROR.HTM" : null
index_document = var.legacy_filenames ? "INDEX.HTM" : null
}
},
]
}
When var.legacy_filenames
is set to true
, the call will override the document filenames. When it is false
, the call will leave the two filenames unspecified, thereby allowing the module to use its specified default values.