TerraformPilot

Troubleshooting

Fix Terraform Error - Inconsistent Conditional Result Types

Fix the Terraform inconsistent conditional result types error. Covers type conversion, null handling, tostring, tolist, and splitting complex conditionals.

LLuca Berton2 min read

Quick Answer

#

The true and false branches of your ternary expression return different types (e.g., string vs number, list vs null). Convert both sides to the same type using tostring(), tolist(), or toset(), or use null which is compatible with any type.

The Error

#

When running terraform plan or terraform validate:

Error: Inconsistent conditional result types
 
  on main.tf line 5, in local:
  5:   value = var.enabled ? var.instance_count : "none"
 
The true and false result expressions must have consistent types.
The 'true' value includes object, and the 'false' value is string.
Error: Inconsistent conditional result types
 
The true result is list of string, but the false result is tuple.

What Causes This Error

#

Terraform is statically typed. A conditional expression (condition ? true_val : false_val) must return the same type from both branches. Terraform cannot infer a common type when they differ.

Common Type Mismatches

#
# String vs Number
var.enabled ? 1 : "disabled"
 
# List vs Empty tuple
var.enabled ? ["a", "b"] : []
 
# Object vs null
var.config != null ? var.config : {}
 
# String vs Number in interpolation
"Count: ${var.enabled ? var.count : "none"}"

How to Fix It

#

Solution 1: Convert Both Sides to the Same Type

#
# BAD — number vs string
locals {
  port = var.use_https ? 443 : "default"
}
 
# GOOD — both strings
locals {
  port = var.use_https ? "443" : "default"
}
 
# GOOD — both numbers
locals {
  port = var.use_https ? 443 : 80
}
 
# Convert explicitly
locals {
  port_string = var.use_https ? tostring(443) : "default"
}

Solution 2: Use Type Conversion Functions

#
# tostring() — convert to string
locals {
  display = var.count > 0 ? tostring(var.count) : "none"
}
 
# tonumber() — convert to number
locals {
  timeout = var.custom_timeout != "" ? tonumber(var.custom_timeout) : 30
}
 
# tolist() — convert to list
locals {
  subnets = var.use_private ? tolist(var.private_subnets) : tolist(var.public_subnets)
}
 
# toset() — convert to set
locals {
  users = var.include_admins ? toset(concat(var.users, var.admins)) : toset(var.users)
}

Solution 3: Handle null Correctly

#

null is compatible with any type, making it the safest "empty" value:

# BAD — string vs empty map
locals {
  config = var.enabled ? var.config_map : {}
}
 
# GOOD — null is type-compatible with any value
locals {
  config = var.enabled ? var.config_map : null
}
 
# Use null for optional resource arguments
resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type
  key_name      = var.enable_ssh ? var.key_pair_name : null  # null = omit the argument
  subnet_id     = var.private ? var.private_subnet : var.public_subnet
}

Solution 4: Fix List vs Tuple Mismatches

#

Empty [] is a tuple, not a list. Convert explicitly:

# BAD — list(string) vs tuple
locals {
  cidrs = var.restrict_access ? var.allowed_cidrs : []
}
 
# GOOD — explicit list type
locals {
  cidrs = var.restrict_access ? var.allowed_cidrs : tolist([])
}
 
# BETTER — use coalesce pattern
locals {
  cidrs = var.restrict_access ? var.allowed_cidrs : ["0.0.0.0/0"]
}

Solution 5: Split Complex Conditionals into Locals

#
# BAD — complex nested conditional
resource "aws_instance" "web" {
  tags = var.environment == "prod" ? {
    Name = "prod-web"
    Backup = "daily"
  } : var.environment == "staging" ? {
    Name = "staging-web"
  } : {
    Name = "dev-web"
  }
}
 
# GOOD — break into locals with consistent types
locals {
  instance_tags = {
    prod = {
      Name   = "prod-web"
      Backup = "daily"
    }
    staging = {
      Name   = "staging-web"
      Backup = "none"      # Same keys as prod
    }
    dev = {
      Name   = "dev-web"
      Backup = "none"      # Same keys as prod
    }
  }
}
 
resource "aws_instance" "web" {
  tags = local.instance_tags[var.environment]
}

Solution 6: Use try() for Optional Values

#
# Instead of a conditional that might have type issues
locals {
  db_host = try(var.external_db_host, aws_db_instance.main[0].endpoint)
}
 
# Safely access nested optional attributes
locals {
  alarm_email = try(var.monitoring.alarm_email, "ops@example.com")
}

Common Type Mismatches and Fixes

#
ExpressionProblemFix
cond ? 1 : "none"number vs stringcond ? "1" : "none"
cond ? list : []list vs tuplecond ? list : tolist([])
cond ? map : {}map vs objectcond ? map : null
cond ? "value" : nullUsually works✅ null is type-compatible
cond ? 0 : nullUsually works✅ null is type-compatible
cond ? set : listset vs listcond ? tolist(set) : list

Troubleshooting Checklist

#
  1. ✅ What types do the true and false branches return?
  2. ✅ Can both be converted to a common type?
  3. ✅ Can you use null instead of an empty value?
  4. ✅ Is [] causing a tuple vs list mismatch?
  5. ✅ Would splitting into a locals block make types clearer?
  6. ✅ Are both branches returning objects with the same keys?

Prevention Tips

#
  • Define explicit variable typestype = string prevents mixed types from sneaking in
  • Use null as the "empty" value — it's compatible with any type
  • Convert empty collections explicitlytolist([]) not []
  • Keep conditional branches simple — complex objects should be in locals or maps
  • Test with terraform validate — catches type errors without hitting the cloud API
#

Conclusion

#

Terraform requires both branches of a conditional to return the same type. Use type conversion functions (tostring, tolist, toset) to align types, use null for optional values, and break complex conditionals into locals with explicit type structures. Running terraform validate catches these errors before plan.

#conditional#types#expressions

Share this article