Using the TypeScript Spread Operator to Clean Up CDK Code

I don’t have much experience with TypeScript—actually, I’d never touched the language until recently when I started writing Infrastructure as Code using CDK. Today I learned a useful trick that can help clean up sections of your stack by putting shared configuration in an object and then using the spread operator to merge those settings into the props for your constructs.

The Problem: Repetitive Lambda Configurations

Imagine you have a stack with a group of Lambda functions that looks something like this:

const firstFunction = new lambda.Function(this, "firstFunction", {
    runtime: lambda.Runtime.PYTHON_3_13,
    code: lambda.Code.fromAsset("lambdas/first_function", {
      bundling: <some_bundling_options>,
    }),
    handler: "first_function.handler",
    vpc: <some_vpc>,
    securityGroups: [sg_1, sg_2, sg_3, sg_n ],
    allowPublicSubnet: true,
    description: "Cool unique description for my first function",
    timeout: cdk.Duration.seconds(17)
  }
);

const secondFunction = new lambda.Function(this, "secondFunction", {
  runtime: lambda.Runtime.PYTHON_3_13,
  code: lambda.Code.fromAsset("lambdas/second_function"),
  handler: "second_function.handler",
  environment: {
    MY_ENV_VAR: <some_value>,
    SOME_OTHER_VAR: <some_value>,
    EVEN_MORE_VAR: <some_value>,
  },
  vpc: <some_vpc>,
  securityGroups: [sg_1, sg_2, sg_3, sg_n ],
  allowPublicSubnet: true,
  description: "Cool unique description for my second function",
  timeout: cdk.Duration.seconds(17)
});

const thirdFunction = new lambda.Function(this, "thirdFunction", {
    runtime: lambda.Runtime.PYTHON_3_13,
    code: lambda.Code.fromAsset("lambdas/third_function"),
    handler: "third_function.handler",
    environment: {
      ANOTHER_VAR: <some_value>,
    },
    vpc: <some_vpc>,
    securityGroups: [sg_1, sg_2, sg_3, sg_n ],
    allowPublicSubnet: true,
    description: "Cool unique description for my third function",
    timeout: cdk.Duration.seconds(17)
  }
);

Looking at these Lambda functions, you’ll notice they share many properties:

  • They all use the same runtime: PYTHON_3_13
  • They’re all attached to the same VPC
  • They all use the same security groups
  • They all have allowPublicSubnet set to true
  • They share the same timeout of 17 seconds

The Solution: Extract Shared Configuration

To reduce duplication, you can extract these common options into a shared configuration object:

const sharedConfig = {
  runtime: lambda.Runtime.PYTHON_3_13,
  vpc: <some_vpc>,
  securityGroups: [sg_1, sg_2, sg_3, sg_n ],
  allowPublicSubnet: true,
  timeout: cdk.Duration.seconds(17),
};

Then, in each Lambda constructor call, use the spread operator to merge the shared configuration:

...sharedConfig

It’s better to see what it looks like in context, so here’s the refactored code:

const sharedConfig = {
  runtime: lambda.Runtime.PYTHON_3_13,
  vpc: <some_vpc>,
  securityGroups: [sg_1, sg_2, sg_3, sg_n ],
  allowPublicSubnet: true,
  timeout: cdk.Duration.seconds(17),
};

const firstFunction = new lambda.Function(this, "firstFunction", {
    ...sharedConfig,
    code: lambda.Code.fromAsset("lambdas/first_function", {
      bundling: <some_bundling_options>,
    }),
    handler: "first_function.handler",
    description: "Cool unique description for my first function",
  }
);

const secondFunction = new lambda.Function(this, "secondFunction", {
  ...sharedConfig,
  code: lambda.Code.fromAsset("lambdas/second_function"),
  handler: "second_function.handler",
  environment: {
    MY_ENV_VAR: <some_value>,
    SOME_OTHER_VAR: <some_value>,
    EVEN_MORE_VAR: <some_value>,
  },
  description: "Cool unique description for my second function",
});

const thirdFunction = new lambda.Function(this, "thirdFunction", {
    ...sharedConfig,
    code: lambda.Code.fromAsset("lambdas/third_function"),
    handler: "third_function.handler",
    environment: {
      ANOTHER_VAR: <some_value>,
    },
    description: "Cool unique description for my third function",
  }
);

Choose Your Shared Configuration Wisely

It’s important to think carefully about what you put into shared configuration. Not everything that appears multiple times is worth extracting. The key principle is to group together things that change together, and leave in place the things that probably won’t.

For example, runtime is a good candidate for shared configuration because you typically want to upgrade the runtime for all Lambda functions at the same time.

However, something like timeout might be better left in individual Lambda definitions. In this example, every timeout was set to 17 seconds, but there’s no inherent reason they should all have the same timeout forever. Timeouts depend on the individual demands placed on each Lambda, so it’s probably better to let each function have its own value.

The distinction is between understanding what truly represents duplicated logic versus what’s just coincidentally the same right now. Just because a setting or piece of code is identical at the moment doesn’t necessarily mean it’s meant to stay duplicated—they might be fundamentally independent behaviors that will likely evolve in different directions. Coupling them together could cause trouble down the road.

Wrapping Up

Anyway, I digress—the main point was to share how useful TypeScript’s spread operator is! I’ll keep learning and sharing discoveries along the way.

Thanks for taking the time to read this. I hope you find it useful!

Juan Luis Orozco Villalobos

Hey there, I'm Juan. A software engineer and consultant currently living in Budapest.