aws / aws/aws-cdk

(aws-cdk-lib): Multiple L2 base classes are not assignable to their interfaces under exactOptionalPropertyTypes

Open
#37,996 14 comments 1 reaction 0 assignees View on GitHub
aws-cdk-lib bug p2
Dominant language
TypeScript
Stars
12.9k
Forks
4.6k
Avg merge
2d 3h
Merged PRs (30d)
83

Description

### Describe the bug

Filing this as a meta/sweep issue to surface a recurring class of bug across `aws-cdk-lib`. Per-module instances have been filed and fixed piecemeal (#19077 in aws-apigateway, fixed; #37600 in aws-cloudwatch, open; #37995 in aws-ec2, just filed). A wider audit shows the same TS2420 violation in **24 base classes across 14 modules**.

The pattern: an `IFoo` interface declares an optional property using `?:` syntax (so the property's *value type* is `T`, not `T | undefined`), while the abstract base class implementing it declares the same field as `T | undefined`. Under [`exactOptionalPropertyTypes: true`](https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html), these are not equivalent: `T | undefined` permits a present-but-`undefined` value that an optional property `T?` does not.

The piecemeal fix has been to flip the offending class field to optional syntax (matching the interface). That works one module at a time but the same bug keeps appearing in new modules. A coordinated sweep would land most of them in one PR, and a regression guard would prevent future recurrence.

### Regression Issue

- [ ] Select this option if this issue appears to be a regression.

### Last Known Working CDK Library Version

_No response_

### Expected Behavior

Concrete L2 classes should be assignable to their declared interfaces under strict TypeScript settings (`strict + exactOptionalPropertyTypes: true`).

### Current Behavior

`tsc` against `aws-cdk-lib@2.254.0` with `strict + exactOptionalPropertyTypes + skipLibCheck: false` (using `typescript@5.6`) emits TS2420 errors in the following 24 base classes across 14 modules:

| Module | Base class file |
|---|---|
| aws-apigateway | `restapi` (`RestApiBase`) |
| aws-appconfig | `configuration` (`ConfigurationBase`) |
| aws-batch | `compute-environment-base` (`ComputeEnvironmentBase`) |
| aws-batch | `job-queue` (`JobQueue`) |
| aws-batch | `managed-compute-environment` |
| aws-batch | `unmanaged-compute-environment` |
| aws-cloudwatch | `widget` (`ConcreteWidget`) — see #37600 |
| aws-codedeploy | `server/deployment-group` |
| aws-ec2 | `vpc` (`VpcBase`) — see #37995 |
| aws-ecs | `cluster` |
| aws-ecs | `ec2/ec2-task-definition` |
| aws-ecs | `external/external-task-definition` |
| aws-ecs | `fargate/fargate-task-definition` |
| aws-elasticloadbalancingv2 | `nlb/network-load-balancer` |
| aws-events | `api-destination` |
| aws-iam | `instance-profile` |
| aws-lambda | `alias` |
| aws-lambda | `lambda-version` |
| aws-secretsmanager | `secret` |
| core | `stack-synthesizers/cli-credentials-synthesizer` |
| core | `stack-synthesizers/default-synthesizer` |
| core | `stack-synthesizers/legacy` |
| core | `stack-synthesizers/stack-synthesizer` |
| pipelines | `blueprint/step` |

Each emits a TS2420 of the form `Class 'X' incorrectly implements interface 'IY'. Types of property '' are incompatible. Type ' | undefined' is not assignable to type ''.`

The bug also surfaces with `skipLibCheck: true` whenever a user passes a concrete class instance to a function/method that takes the corresponding interface (TS2379 at the call site).

### Reproduction Steps

```sh
mkdir repro && cd repro
npm init -y
npm i aws-cdk-lib constructs typescript
cat > tsconfig.json <<'JSON'
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": false,
"noEmit": true
},
"include": ["src/**/*"]
}
JSON
mkdir src && echo "import 'aws-cdk-lib';" > src/repro.ts
npx tsc
```

Yields TS2420 errors across the 14 modules listed above.

### Possible Solution

Apply the same fix shape used in #19077 across all affected base classes: change the class field/getter from `T | undefined` to optional-property syntax matching the interface (`?: T`). This is a one-line change per field, so the sweep is mechanical.

Codemod outline:

1. For each `(class X implements IY)` pair, find every getter/field on `X` whose type is `T | undefined` and whose corresponding interface property on `IY` is declared `?: T`.
2. Change the class field/getter to `?: T` (and adjust assignments to use undefined-aware patterns where needed).

### Possible Regression Guards

To prevent the same bug recurring in future L2 modules, three options ordered by ambition:

1. **CI snapshot test (smallest, immediate value).** Add a job that runs `tsc` over a tiny consumer fixture that imports `aws-cdk-lib` against `strict + exactOptionalPropertyTypes + skipLibCheck: false` (the repro shape above). Doesn't change `aws-cdk-lib`'s own build; just verifies the published typings hold up under strict consumption. Easy to extend when new modules ship.

2. **Enable `exactOptionalPropertyTypes` in `aws-cdk-lib`'s own tsconfig (medium).** The direct guard — if the lib itself doesn't compile under the flag, no violating L2 can ship. Requires the sweep to land first, and adds a strictness commitment the team would need to defend. Strongest backstop once in place.

3. **Custom lint rule (largest).** Flag, at code-review time, any class member typed `T | undefined` whose corresponding interface property is declared `?: T`. Catches the bug closest to the source. Implementation note: this needs cross-file type information (interface vs implementing class), so it'd be a TypeScript-aware ESLint rule or a one-off type-aware script, not a pure-AST lint.

Option 1 is the quickest win and complements either of the other two. Option 2 is the cleanest long-term posture if the maintainers are comfortable with the strictness commitment. Option 3 is the most ergonomic for contributors but the most work to build.

### Additional Information/Context

Prior art / context:
- #19077 (aws-apigateway) — fixed in 2023, established the fix pattern.
- #37600 (aws-cloudwatch) — open, has confirmed diagnosis from `pahud` and a 2-line fix.
- #37995 (aws-ec2) — just filed.

`exactOptionalPropertyTypes: true` is increasingly common in TS strict-mode projects since ~TS 4.4. Without a regression guard, the bug is likely to recur in new L2 modules.

### AWS CDK Library version (aws-cdk-lib)

v2.254.0

### AWS CDK CLI version

N/A

### Node.js Version

v24

### OS

macOS

### Language

TypeScript

### Language Version

TypeScript 5.6

### Other information

_No response_

Contributor guide

Open the contributing guide

Research direction

Start with the 24 base class files listed across aws-apigateway, aws-appconfig, aws-batch, aws-cloudwatch, aws-codedeploy, aws-ec2, aws-ecs, aws-elasticloadbalancingv2, aws-events, aws-iam, aws-lambda, aws-secretsmanager, core, and pipelines. Compare each class with its interface, using #19077 as prior art, then run the provided strict TypeScript consumer reproduction. Done means the listed TS2420 failures are resolved and an agreed regression guard verifies exactOptionalPropertyTypes compatibility.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, developer-experience
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.