I'm working on an AWS CDK project using TypeScript and I'm trying to improve type safety for a parameter tracking system I've implemented. I have a base class that allows child stacks to export parameters, and I want to ensure type safety when accessing these parameters later in my code. Here's a simplified version of my current code:
// BaseStack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
export interface BaseStackProps extends cdk.StackProps {
// Base props
}
export class BaseStack extends cdk.Stack {
// Used to export parameters for cross-region references
exportedParameters = {}
constructor(scope: Construct, id: string, props: BaseStackProps) {
super(scope, id, props);
}
exportParameter(name: string, value: string) {
new StringParameter(this, name, {
parameterName: name,
stringValue: value
});
this.exportedParameters[name] = name;
}
}
// StackA.ts
export class StackA extends BaseStack {
constructor(scope: Construct, id: string, props: BaseStackProps) {
super(scope, id, props);
// some custom resources
this.exportParameter("specialParameter", "value");
}
}
// app.ts
const stackA = new StackA(this, "stackA", {});
// Problem: I want TypeScript to catch this error:
stackA.exportedParameters.nonExistentParameter; // No TypeScript error, but would fail at runtime
// And I want autocomplete to show me the available parameters:
stackA.exportedParameters.specialParameter; // Should work and be suggested by IntelliSense
What I'm looking for:
stackA.exportedParameters
.Is there a proper TypeScript pattern to achieve this kind of type safety for dynamically added properties in inherited classes?
As with many cases I suggest to avoid using constructors and use factories rather, that gives much more flexibility to typing:
export class BaseStack<T extends Record<string, StringParameter> = {}> extends cdk.Stack {
// Used to export parameters for cross-region references
exportedParameters = {} as T;
protected constructor(scope: Construct, id: string, props: BaseStackProps) {
super(scope, id, props);
}
static create(scope: Construct, id: string, props: BaseStackProps){
return new this(scope, id, props);
}
exportParameter<N extends string>(name: N, value: string) {
const param = new StringParameter(this, name, {
parameterName: name,
stringValue: value
});
(this as any).exportedParameters[name] = value; // you could find a better solution
return this as this & BaseStack<{[k in N]: typeof param}>;
}
}
// StackA.ts
export class StackA extends BaseStack {
static create(scope: Construct, id: string, props: BaseStackProps){
const out = new StackA(scope, id, props);
return out
.exportParameter("specialParameter", "value")
.exportParameter("specialParameter2", "value2");
}
}
// app.ts
const stackA = StackA.create(this, "stackA", {});