Hello there

My current technology stack: .NET 9, Python, TypeScript, and Azure.

I develop microservices and terraform of different sizes. Sharing my challenges and key learning.

About

The views expressed in this blog are my own and do not reflect my employer's. I am not responsible for any consequences of using the information provided. This blog is for educational purposes only, not for commercial use. Readers should apply their own judgment.

Pulumi TypeScript: Escaping the Input<Input<T>>[] | undefined Type Hell

April 06, 2025 Dipankar Haldar 33 people viewed this post

Spoiler alert: Pulumi's type system is powerful... but when it goes rogue, it can ruin your deployment and your weekend.

When Infrastructure Becomes a Labyrinth

A few weeks ago, I was building a highly dynamic Azure Front Door setup using Pulumi and TypeScript. Nothing too fancy — just trying to create:

  • Multiple custom domains
  • Several endpoints
  • Individual backend pools and origins per endpoint
  • Custom route rules with caching policies
  • Full-blown Web Application Firewall (WAF) integration

The goal? Take a simple configuration array and turn it into a full-fledged global load balancer with security baked in.

Everything was dynamic — the number of endpoints, their paths, their backend logic — and I was feeling good. Confident. Invincible.

Until I hit The Type Hell.


The Code That Broke My Sanity

Let’s say you have something like this:

const frontdoorConfigs = [
  { name: "web", enabled: true, host: "web.mysite.com", backend: "webapp" },
  { name: "api", enabled: false, host: "api.mysite.com", backend: "apigateway" },
];

const backends = frontdoorConfigs.map(cfg => cfg.enabled ? createBackend(cfg) : undefined);

frontdoorArgs.backends = backends;

Seems fine, right? But Pulumi throws this gem:

Type 'Input<Input<Backend>[]> | undefined' is not assignable to type 'Input<Input<Backend>[]>'.

Wait, what? Isn’t Input recursive? Shouldn’t Input<Input> just collapse into Input?

Nope. This is Pulumi’s strict typing model screaming for help.


Understanding Pulumi's Input Type

Pulumi’s Input isn’t just a value; it’s a future — a lazy, possibly unresolved promise of a value. And when you layer those futures with arrays, conditionals, and mapping logic, things start to break.

What Pulumi expects:

Input<T[]>

What you often pass:

(Input<T> | undefined)[]

Or worse:

Input<Input<T>[]> | undefined

The type checker can’t resolve that cleanly — and neither can Pulumi at deployment time.


The Solution: Smart Filtering + Mapping

After lots of trial, error, and caffeine, I landed on a robust utility function that saved my setup:

export function mapFilterDefined<T, U>(
  arr: Input<T[]>,
  fn: (t: T) => U | undefined
): Input<U[]> {
  return pulumi.output(arr).apply(list =>
    list.map(fn).filter((x): x is U => x !== undefined)
  );
}

Then I updated the logic to:

const backendInputs = mapFilterDefined(frontdoorConfigs, cfg => {
  if (!cfg.enabled) return undefined;
  return createBackend(cfg);
});

frontdoorArgs.backends = backendInputs;

Boom. Everything works. TypeScript stops yelling. Pulumi previews correctly. Deployments succeed.


Real Example: Dynamic Front Door with Pulumi

Here’s a simplified view of what I was doing:

const endpoints = mapFilterDefined(frontdoorConfigs, cfg => {
  if (!cfg.enabled) return undefined;
  return new azure.cdn.FrontdoorEndpoint(cfg.name, {
    endpointName: cfg.name,
    hostName: cfg.host,
    ...
  });
});

const routes = mapFilterDefined(frontdoorConfigs, cfg => {
  if (!cfg.enabled) return undefined;
  return new azure.cdn.Route(`${cfg.name}-route`, {
    ...route logic...
  });
});

// etc for policies, origins, etc

Each step filtered out disabled configs and ensured only clean, fully-resolved inputs were passed to Pulumi resources.


Key Lessons Learned

  • Don’t let undefined sneak into your Pulumi arrays.
  • Be wary of Input<Input> — use pulumi.output().apply() to flatten and resolve values.
  • Build utility functions to avoid repeating logic and clean up complex flows.
  • Always check the actual expected type in the Pulumi docs or source code — TS errors can be misleading.

Closing Thoughts

This isn’t just a TypeScript problem — it’s a Pulumi lifecycle problem. When you defer evaluation until deployment, your types need to be perfectly predictable.

In the end, I deployed a secure, scalable, and elegant Azure Front Door setup — but only after learning how to wrangle types as fiercely as infra.

If you're hitting a wall with Pulumi's types, remember: it’s not just you. Type Hell is real — but it’s also conquerable.

🧠 Happy infra building, and may your Inputs always resolve the way you intend.