Advanced TypeScript Patterns in Terraform CDK
/ 4 min read
Series Navigation
- Part 1: Getting Started with Terraform CDK
- Part 2: Resource Management with CDK
- Part 3: Advanced TypeScript Patterns (Current)
- Part 4: Custom Constructs and Components
- Part 5: Testing CDK Applications
- Part 6: CI/CD for CDK Projects
Advanced TypeScript Patterns for CDKTF
This post explores advanced TypeScript patterns that can help you write more maintainable, type-safe, and reusable infrastructure code with CDKTF.
Generics and Type Constraints
Resource Factory Pattern
interface ResourceConfig<T> { name: string; tags: Record<string, string>; properties: T;}
interface EC2Config { instanceType: string; ami: string;}
class ResourceFactory<T> { static create<T>( scope: Construct, config: ResourceConfig<T>, creator: (scope: Construct, id: string, props: T) => any ) { return creator(scope, config.name, { ...config.properties, tags: config.tags, }); }}
// Usageconst ec2Config: ResourceConfig<EC2Config> = { name: "web-server", tags: { Environment: "prod" }, properties: { instanceType: "t2.micro", ami: "ami-123456", },};
const instance = ResourceFactory.create( this, ec2Config, (scope, id, props) => new Instance(scope, id, props));Abstract Classes and Inheritance
Base Stack Pattern
abstract class BaseStack extends TerraformStack { protected readonly config: StackConfig; protected readonly provider: AwsProvider;
constructor(scope: Construct, id: string, config: StackConfig) { super(scope, id); this.config = config; this.provider = new AwsProvider(this, "AWS", { region: config.region, }); }
protected abstract createResources(): void;
protected getDefaultTags(): Record<string, string> { return { Environment: this.config.environment, ManagedBy: "CDKTF", Project: this.config.projectName, }; }}
class NetworkStack extends BaseStack { protected createResources(): void { const vpc = new Vpc(this, "VPC", { cidrBlock: this.config.vpcCidr, tags: this.getDefaultTags(), }); // Additional resources... }}Decorators for Resource Configuration
Validation Decorators
function validateCidr() { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { const cidr = args[0]; const cidrRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/; if (!cidrRegex.test(cidr)) { throw new Error(`Invalid CIDR block: ${cidr}`); } return originalMethod.apply(this, args); }; };}
class NetworkConfig { @validateCidr() setVpcCidr(cidr: string) { this.vpcCidr = cidr; }}Union Types and Type Guards
Resource Type Safety
type ResourceType = "vpc" | "subnet" | "instance";type ResourceConfig = VpcConfig | SubnetConfig | InstanceConfig;
interface BaseConfig { type: ResourceType; name: string;}
interface VpcConfig extends BaseConfig { type: "vpc"; cidrBlock: string;}
interface SubnetConfig extends BaseConfig { type: "subnet"; vpcId: string; cidrBlock: string;}
function isVpcConfig(config: ResourceConfig): config is VpcConfig { return config.type === "vpc";}
class ResourceManager { createResource(config: ResourceConfig) { if (isVpcConfig(config)) { return new Vpc(this, config.name, { cidrBlock: config.cidrBlock, }); } // Handle other resource types... }}Utility Types
Partial Configuration
interface SecurityGroupRule { protocol: string; fromPort: number; toPort: number; cidrBlocks: string[];}
type PartialRule = Partial<SecurityGroupRule>;
class SecurityGroupBuilder { private rules: SecurityGroupRule[] = [];
addRule(rule: PartialRule): this { const defaultRule: SecurityGroupRule = { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"], };
this.rules.push({ ...defaultRule, ...rule }); return this; }
build(scope: Construct, id: string): SecurityGroup { return new SecurityGroup(scope, id, { ingress: this.rules, }); }}Advanced Mapping Types
Resource Tag Mapping
type RequiredTags = "Environment" | "Project" | "ManagedBy";type OptionalTags = "Owner" | "CostCenter";
type Tags = Record<RequiredTags, string> & Partial<Record<OptionalTags, string>>;
class TaggableResource { protected validateTags(tags: Tags) { const requiredTags: RequiredTags[] = [ "Environment", "Project", "ManagedBy", ];
for (const tag of requiredTags) { if (!tags[tag]) { throw new Error(`Missing required tag: ${tag}`); } } }}Async Patterns
Resource Dependencies
class AsyncResourceManager { private readonly resourcePromises: Map<string, Promise<any>> = new Map();
async createVpc(config: VpcConfig): Promise<Vpc> { const vpc = new Vpc(this, config.name, config); this.resourcePromises.set(config.name, Promise.resolve(vpc)); return vpc; }
async createSubnet(config: SubnetConfig): Promise<Subnet> { const vpc = await this.resourcePromises.get(config.vpcName); if (!vpc) { throw new Error(`VPC ${config.vpcName} not found`); }
return new Subnet(this, config.name, { ...config, vpcId: vpc.id, }); }}Best Practices
-
Type Safety
- Use strict TypeScript configuration
- Leverage type inference
- Create custom type guards
-
Code Organization
- Use abstract classes for common patterns
- Implement the builder pattern for complex resources
- Create utility functions for repeated operations
-
Error Handling
- Create custom error types
- Use type guards for runtime checks
- Implement validation decorators
-
Testing
- Write unit tests for utility functions
- Test type constraints
- Mock AWS resources for testing
Next Steps
In Part 4: Custom Constructs and Components, we’ll explore how to create reusable components and custom constructs using these advanced TypeScript patterns.