Repository navigation
Type-check the ports passed to getMappedPort #1583
Copy link
Copy link
Open
Labels
enhancementNew feature or requestNew feature or requestminorBackward compatible functionalityBackward compatible functionality
Description
What happens
getMappedPort accepts any number. Asking for a port that was never exposed compiles, and fails when the test runs:
const container = await new GenericContainer(image).withExposedPorts(8080).start();
container.getMappedPort(8081); // compiles
// Error: No port binding found for :8081/tcpThe mistake is usually a typo, or a port constant that drifted from the one passed to withExposedPorts.
Expected
The ports given to withExposedPorts are carried in the builder's type, and getMappedPort on the started container accepts only those:
const container = await new GenericContainer(image).withExposedPorts(8080, "53/udp").start();
container.getMappedPort(8080);
container.getMappedPort(53, "udp");
container.getMappedPort(8081); // compile errorWhere the ports are not known statically, nothing changes: getMappedPort accepts any number, as it does today.
Where
withExposedPortsandstartinpackages/testcontainers/src/test-container.tsandpackages/testcontainers/src/generic-container/generic-container.tsgetMappedPortonStartedTestContainer
Notes
A prototype of the types compiles under the repo's TypeScript (7.0.2) with these cases checked. It needs no class type parameter, so the fluent this return type and module subclasses are unaffected:
declare const exposed: unique symbol;
type Exposed<P extends number> = { readonly [exposed]: { readonly [K in P]: true } };
type PortsOf<T> = T extends { readonly [exposed]: infer M } ? keyof M & number : number;
interface StartedTestContainer<P extends number = number> {
getMappedPort(port: P, protocol?: string): number;
getMappedPort(portWithProtocol: `${P}/${"tcp" | "udp"}`): number;
}
class GenericContainer {
withExposedPorts<const T extends PortWithOptionalBinding[]>(...ports: T): this & Exposed<ContainerPort<T[number]>>;
start(): Promise<StartedTestContainer<PortsOf<this>>>;
}- Each
withExposedPortscall intersects a marker ontothis. Laterwith*calls returnthis, so the marker survives the rest of the chain, and repeated calls accumulate. - It falls back to
number, and so to today's behaviour, when a port is anumbervariable, when the builder is stored in a variable typedGenericContainer, and whenwithExposedPortswas never called. - Module containers are unaffected, because their
start()returns their own started type. A module could opt in later. - Ports that a subclass exposes in its constructor or hooks are not visible to the caller's type.
- The
consttype parameter needs TypeScript 5.0 or later in consuming projects. Without it, only the object form ({ container, host }) loses precision. - The intersection shows up in editor hovers, for example
GenericContainer & Exposed<8080 | 53>. - Changing the signature on the
TestContainerinterface is a compile error for anyone who implements that interface without extendingGenericContainer.
Dependencies
- Nothing blocks this. It can start now.
- Coordinate with Add the missing builder methods to the
TestContainerinterface #1529: both change theTestContainerinterface. - Coordinate with Remove the need for
AbstractStartedContainerwrappers in modules #1563: the started type gains a type parameter, which the forwarding wrapper has to pass through until it is removed.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestminorBackward compatible functionalityBackward compatible functionality
Activity