The true and false result expressions must have consistent types: working around Terraform's type system

Many people struggle with how the ternary operator works with Terraform’s type system. Ternary operators won’t compile unless both true and false expressions have exactly the same type. See Inconsistent conditional result types fails for complex types · Issue #22405 · hashicorp/terraform · GitHub, amazon web services - The true and false result expressions must have consistent types. The given expressions are list of string and number, respectively - Stack Overflow, The true and false result expressions must have consistent types. The given expressions are object and tuple, respectively

However, in many cases it is ok that they don’t have the same exact type. Here is an example, trying to manipulate yaml data loaded from a file (m is an item from a yaml list, this is inside a [for m in … : …] construct:

This is not ok:
((length(var.regions) > 0) ? merge(m, { regions: var.regions }) : m)

This is ok: yamldecode(((length(var.regions) > 0) ? yamlencode(merge(m, { regions: var.regions })) : yamlencode(m)))

The workaround of encoding and decoding works, but is really ugly. Wouldn’t it be possible to somehow cast the expressions to the any type instead?

Hi @gpothier,

Perhaps a different way to write this would be to have your “else” case produce a value of the same type where the set(?) of regions is empty?

merge(
  m,
  { regions = (
      length(var.regions) > 0 ? toset(var.regions) : toset([])
  ) }
) 

Of course, that’s redundant since an empty var.regions would produce the same result if passed to toset anyway, and so the following would be a more concise way to get there without using a condition at all:

merge(
  m,
  { regions = toset(var.regions) }
) 

This means that the result will always have the regions attribute but it might be empty. I know that’s not the same thing that your yamlencode/yamldecode example achieves, but having data structures only differ dynamically in their values and not in their types is the “main case” Terraform is intended to deal with, similar to other statically-typed languages.

any is a type constraint rather than an actual type, so there are no actual values of that type. Rather than “working around” the type system, I’d suggest working with it by deciding what type each of your values ought to have and then being consistent about it.

While as you’ve seen Terraform does have some dynamic-typing “escape hatches” as a matter of pragmatism – a deserialization function would be much harder to use if you always had to specify statically which type it will produce – Terraform is primarily a structural-statically-typed language with type inference and I’d recommend engaging with it in that way rather than trying to treat it as if it were a fully-dynamically-typed language.

In most cases (though I expect there will always be examples to the contrary), writing expressions that are consistently typed should produce a more concise and explicit result than trying to cheat Terraform’s type checker via the serialization functions.