typed-soap
v0.0.5
Published
End-to-end type-safe SOAP client for TypeScript. Generate typed RPC wrappers directly from WSDLs.
Maintainers
Readme
typed-soap
Runtime client for type-safe SOAP calls in TypeScript. Wraps the battle-tested soap package and adds end-to-end type safety, a cleaner async API, and automatic numeric deserialization.
Use with tsoap-cli to generate types from your WSDL.
Why
The soap package works, but the developer experience is rough:
import soap from 'soap';
const client = await soap.createClientAsync('http://example.com?wsdl');
// No types, 4-tuple return, easy to get wrong
const [result] = await client.GetWeatherAsync({ city: 'NYC' });
// ^? anytyped-soap gives you a typed proxy that mirrors the WSDL structure and returns results directly:
import { createWeatherServiceClient } from './generated/weather.js';
const client = await createWeatherServiceClient('http://example.com?wsdl');
const result = await client.WeatherService.WeatherPort.GetWeather({ city: 'NYC' });
// ^? { temperature: number; description: string }Install
# npm
npm install typed-soap
# pnpm
pnpm add typed-soap
# yarn
yarn add typed-soapTo generate the types from a WSDL, also install the CLI as a dev dependency:
npm install -D tsoap-cliQuick start
1. Generate types (one-time setup)
npx tsoap generate -i ./weather.wsdl -o ./src/generatedThis produces src/generated/weather.ts with TypeScript interfaces and a typed factory function.
2. Create a client and call operations
import { createWeatherServiceClient } from './src/generated/weather.js';
const client = await createWeatherServiceClient('http://example.com/weather?wsdl');
// Service -> Port -> Operation — fully typed at every level
const result = await client.WeatherService.WeatherPort.GetWeather({ city: 'NYC' });
console.log(result.temperature); // numberAPI
createSoapClient<T>(wsdlUrl, options?)
The core function. The generic parameter T is a ServiceDefinition type generated by tsoap-cli.
import { createSoapClient } from 'typed-soap';
import type { WeatherServiceDefinition } from './generated/weather.js';
const client = await createSoapClient<WeatherServiceDefinition>(
'http://example.com/weather?wsdl',
);In most cases you won't call this directly — use the generated factory function instead (e.g. createWeatherServiceClient), which passes the generic for you.
Options
SoapClientOptions extends the soap package's IOptions, so all standard soap options are supported. One addition:
| Option | Type | Description |
|---|---|---|
| endpoint | string | Override the SOAP endpoint URL from the WSDL |
| customDeserializer | Record<string, (text: string) => unknown> | Merge additional custom deserializers (on top of the built-in ones) |
const client = await createWeatherServiceClient(wsdlUrl, {
endpoint: 'https://production.example.com/soap',
wsdl_headers: { Authorization: 'Bearer ...' },
});Exported types
| Export | Description |
|---|---|
| createSoapClient<T>() | Factory function to create a typed client |
| ServiceDefinition | Base type for generated service definitions |
| OperationDefinition | Shape of a single operation ({ input, output }) |
| InferClient<T> | Mapped type that converts a ServiceDefinition into callable methods |
| SoapClientOptions | Options passed to createSoapClient |
| TSOAP_BRAND | Symbol.for("tsoap.client") — for runtime brand checking |
InferClient<T>
This is the type-level magic. It transforms a static service definition into a nested object of async functions:
type InferClient<T extends ServiceDefinition> = {
[S in keyof T]: {
[P in keyof T[S]]: {
[O in keyof T[S][P]]: (input: T[S][P][O]["input"]) => Promise<T[S][P][O]["output"]>;
};
};
};You never need to use this directly — the generated factory returns it for you.
How it differs from soap
| Aspect | soap package | typed-soap |
|---|---|---|
| Call pattern | client.OpAsync(args) | client.Service.Port.Op(args) |
| Return value | [result, rawResponse, soapHeader, rawRequest] | result only |
| Types | Everything is any | Full end-to-end types from WSDL |
| Autocomplete | None | Service, port, and operation names |
| Typo detection | Runtime failure (or silent wrong data) | Compile-time error |
| Numeric XSD types | unsignedInt, byte, etc. arrive as raw strings | Automatically deserialized to number |
Built-in deserializers
The soap package handles int, long, float, double, decimal, boolean, dateTime, and date. It leaves other numeric types as raw strings. typed-soap fills the gaps:
| XSD type | Deserialized to |
|---|---|
| byte | number |
| unsignedByte | number |
| unsignedShort | number |
| unsignedInt | number |
| unsignedLong | number |
| negativeInteger | number |
| nonNegativeInteger | number |
| positiveInteger | number |
| nonPositiveInteger | number |
You can add your own deserializers via the customDeserializer option — they merge on top of the built-ins.
Introspection
The proxy provides helpers at each level for debugging and exploration:
// List service names
client.$services; // ["WeatherService"]
// List port names
client.WeatherService.$ports; // ["WeatherPort"]
// List operation names
client.WeatherService.WeatherPort.$operations; // ["GetWeather"]
// Get the raw WSDL description at any level
client.$describe();
client.WeatherService.$describe();
client.WeatherService.WeatherPort.$describe();Brand checking
Detect a typed-soap client at runtime:
import { TSOAP_BRAND } from 'typed-soap';
if (TSOAP_BRAND in someObject) {
// it's a typed-soap client
}Architecture
Under the hood, createSoapClient creates a standard soap.Client and wraps it in a 3-level nested Proxy:
- Level 1 — service name lookup, returns a port proxy
- Level 2 — port name lookup, returns an operation proxy
- Level 3 — operation name lookup, returns an async function that calls
client[service][port][opAsync](input)and returns only the first element of the response tuple
Unknown property accesses return undefined (no throws). TypeScript's InferClient<T> provides compile-time safety; the allowlist pattern keeps the proxy compatible with loggers, test frameworks, bundlers, and devtools.
Requirements
- Node.js 18+
soap^1.1.6 (peer dependency, installed automatically)
License
MIT
