@commerce-klaus/typescript-sfcc
TypeScript tooling for Salesforce Commerce Cloud cartridge projects.
The package currently ships two main entry points:
- a tsserver plugin that resolves SFCC-specific module patterns such as
~/...,*/..., cartridge aliases, andmodule.superModule - a CLI that typechecks cartridge projects with the same resolution behavior
What Commerce Klaus adds
The official Salesforce B2C Commerce IDE Extension is the preferred foundation for Script API IntelliSense and standard cartridge module resolution in VS Code. The broader Salesforce B2C Developer Tooling also bundles XSD schemas and offers XML validation through its CLI; this checks document structure rather than generating JavaScript types. Commerce Klaus adds generated project contracts and checks that can run without an editor:
| Area | Commerce Klaus addition | Practical benefit |
|---|---|---|
| Project contracts | Projects metadata, hooks.json, Custom API schemas, and steptypes.json into TypeScript declarations | Configuration and JavaScript implementation are checked together |
| Super modules | Resolves and types module.superModule chains | Cartridge overrides remain navigable and type-safe |
| Type checking | Runs the cartridge-aware TypeScript compiler from the command line | The same class of diagnostics can block CI without a VS Code extension |
| Registration validation | Checks hook descriptors, referenced scripts, and required CommonJS exports | Broken registrations fail before deployment |
| Shared tooling | Reuses the same cartridge semantics across TypeScript, ESLint, Vite, Babel, and Vitest packages | Local tools agree about which cartridge implementation wins |
Keep the JavaScript, add the guarantees
SFCC projects often contain years of production-tested CommonJS JavaScript. Rewriting that code in TypeScript would introduce a compilation step between the source developers inspect and the code running in the sandbox. That can make deployment artifacts, production stack traces, and urgent debugging harder to follow.
This package takes an incremental route: cartridge code remains runnable JavaScript, while TypeScript checks it through JSDoc and generated declarations. Existing modules can gain useful guarantees one function at a time, without a source transformation or a framework migration.
The generated types are especially valuable because they come from contracts the project already maintains:
- site metadata defines custom and system object attributes
hooks.jsonand Salesforce Script API declarations define hook boundaries- Custom API schemas define request and response shapes
steptypes.jsondefines job parameters, lifecycle functions, and status codes
This keeps annotations small and reduces duplicated declarations that can drift away from the platform configuration. Prefer a generated function contract over repeating every parameter and return type:
/** @type {SfccJobSteps.Definitions["custom.ExportCatalog"]["Functions"]["execute"]} */
const execute = function (parameters, stepExecution) {
// parameters and stepExecution are inferred from steptypes.json
}Type checking complements the ESLint compatibility rules. Types catch mismatched project contracts and Script API usage; lint rules reject syntax, globals, module patterns, and standard-library features that are not safe for the configured SFCC runtime. Together they move failures closer to the edit that introduced them, while leaving the deployed JavaScript recognizable.
For the broader modernization strategy, see Modernize SFCC JavaScript with confidence.
Commerce Klaus can read these options from one shared commerce-klaus.config.ts or .js file. Package options remain supported and override the central values.
Install
pnpm add -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-cliyarn add -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-clinpm install -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-clivp install -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-cliSFCC Script Types Setup
@commerce-klaus/typescript-sfcc is compatible with Salesforce b2c-script-types output.
For dw/* imports, the tool resolves types from a vendored workspace path:
.b2c-script-types/types/dw/*
The path is calculated relative to each project config, so both setups are supported:
cartridges/jsconfig.jsoncartridges/<cartridge>/jsconfig.json
Example workflow with the Salesforce B2C Developer Tooling CLI:
b2c setup ide vscode-types --copy --force --output .b2c-script-types/jsconfig.generated.jsonNotes:
sfcc-dtsis not required fordw/*resolution in this package.- Keep
.b2c-script-types/typesavailable in the workspace before runningsfcc-ts-typecheck.
Custom Attributes
sfcc-ts-sync-typesextendsb2c-script-typesby reading all XML files under<siteTemplatePath>/meta/*.xml(default:sites/site_template/meta/*.xml) and generating custom attribute declarations into.b2c-script-types/types/sfcc-custom-attributes.generated.d.ts.- Custom Object and System Object attribute interfaces are available by name to JavaScript JSDoc from
.b2c-script-types/types/sfcc-custom-attributes.generated.d.ts, for exampleCustomObjectExampleNotificationCustomAttributesandProductCustomAttributes. - The generated declarations are consumed by both
sfcc-ts-typecheckand the tsserver plugin, so editor diagnostics and CLI diagnostics use the same custom attribute typing. - The generated declarations also patch
dw/object/SystemObjectMgrso supportedtypeliterals narrowgetAllSystemObjects,querySystemObject, andquerySystemObjectsto the matching system object class. - Custom attribute values are inferred by metadata type. For enums,
select-multiple-flag=trueis treated as a multi-value enum and emitted asSfccEnumValue<T>[]. - Attribute type values follow Salesforce metadata schema (
metadata.xsd), where enum multi-select is modeled viaenum-of-*plusselect-multiple-flag=true(not viaset-of-enum-of-*).
Custom attribute mapping reference:
| Metadata type | Generated TypeScript type |
|---|---|
boolean | boolean |
date, datetime | Date |
double, int, integer, long, number, quantity | number |
email, password, string, text, url | string |
html | dw.content.MarkupText |
enum-of-string (with value-definitions) | SfccEnumValue<"value1" | "value2" ...> |
enum-of-int (with value-definitions) | SfccEnumValue<1 | 2 ...> |
enum-of-* with select-multiple-flag=true | SfccEnumValue<...>[] |
enum-of-* (without value-definitions) | SfccEnumValue<baseType> |
unknown enum-of-* | SfccEnumValue<string | number> |
set-of-string | string[] |
set-of-int, set-of-double | number[] |
invalid set-of-enum-of-* (not in metadata.xsd) | unknown[] |
unknown set-of-* | unknown[] |
| unsupported/unknown single-value type | unknown |
Example (Product.custom):
import ProductMgr = require("dw/catalog/ProductMgr")
const product = ProductMgr.getProduct("my-product-id")
if (product) {
// enum-of-string -> SfccEnumValue<...>
const status = product.custom.status
if (status) {
const value = status.getValue()
const label = status.getDisplayValue()
void value
void label
}
// enum-of-int + select-multiple-flag=true -> SfccEnumValue<...>[]
const modes = product.custom.modes
if (modes && modes.length > 0) {
const firstModeValue = modes[0].getValue()
void firstModeValue
}
// unknown custom attribute -> TypeScript error
// @ts-expect-error Property 'doesNotExist' does not exist
product.custom.doesNotExist
}Named Custom Object attribute types can be referenced directly from JavaScript JSDoc:
// @ts-check
/**
* @param {CustomObjectExampleNotificationCustomAttributes} custom
* @returns {string | undefined}
*/
function readNotification(custom) {
return custom.eventCode
}System Object attributes use the same pattern:
// @ts-check
/**
* @param {ProductCustomAttributes} custom
* @returns {string | undefined}
*/
function readProductOrigin(custom) {
return custom.origin
}Schema-driven assumptions:
- The generator follows the Salesforce metadata schema namespace
http://www.demandware.com/xml/impex/metadata/2006-10-31. - Type inference uses these fields from attribute definitions:
typevalue-definitionsandvalue-definition/valueselect-multiple-flag(for enum multi-select)mandatory-flag(required vs optional property)
- Supported metadata roots are
type-extension/custom-attribute-definitionsandcustom-type/attribute-definitions. type-extensionobject types are assigned todw/*modules via a preferred mapping table for known SFCC system objects (for exampleProduct->dw/catalog/Product,Order->dw/order/Order).- If no preferred mapping exists, a system object type is only assigned when its
dw/*basename match is unique; ambiguous matches are skipped. - For enum attributes, multi-value behavior is derived from
select-multiple-flag=true. - For set attributes, valid values follow
metadata.xsd(set-of-string,set-of-int,set-of-double); non-schema variants fall back tounknown[].
Hook Handling
sfcc-ts-sync-types generates .b2c-script-types/types/sfcc-hooks.generated.d.ts with SfccHooks aliases for Salesforce system hook interfaces available in the vendored Script API types. For example, SfccHooks.OrderCalculate refers to the dw.order.calculate signature.
Existing Salesforce system hook signatures can be used from JavaScript JSDoc without duplicating their parameters or return type:
// @ts-check
/** @type {SfccHooks.OrderCalculate} */
const calculate = function (lineItemCtnr) {
// lineItemCtnr is typed from dw/order/hooks/CalculateHooks
}
exports.calculate = calculateSfccHooks includes only hook signatures declared by the synchronized Salesforce Script API types. Shopper API hook documents and project-specific c_* response fields remain project-local types.
For Shopper API hooks, add an optional sfcc-hooks.d.ts file next to cartridges/jsconfig.json. It is loaded automatically by both sfcc-ts-typecheck and the tsserver plugin, even when cartridge project configs include JavaScript files only. Keep document models narrow and declare only the fields read or written by the hook:
export {}
declare global {
namespace SfccHooks {
type ShopperProductModifyGetResponse = (document: { c_brand: string }) => void
}
}Use the project-local alias from the hook implementation:
// @ts-check
/** @type {SfccHooks.ShopperProductModifyGetResponse} */
const modifyGETResponse = function (document) {
document.c_brand = "Example"
}
exports.modifyGETResponse = modifyGETResponseTypes that only apply to a single cartridge can instead be declared in an sfcc-hooks.d.ts file directly inside that cartridge, next to its own jsconfig.json (for example cartridges/int_storepickup/sfcc-hooks.d.ts). All discovered files share the same global SfccHooks namespace, so cartridge-specific and cartridges-wide declarations can be mixed:
cartridges/sfcc-hooks.d.ts: shared across all cartridgescartridges/<cartridge>/sfcc-hooks.d.ts: specific to that cartridge only
Migration from 1.x: this package previously loaded a single sfcc-hooks.d.ts at the workspace root (one level above cartridges/). Move that file into cartridges/ to keep it working.
Recommended package.json scripts:
{
"scripts": {
"types:sfcc:sync": "sfcc-ts-sync-types --min-version 26.7.0",
"types:sfcc:sync:force": "sfcc-ts-sync-types --force",
"prepare": "sfcc-ts-sync-types --min-version 26.7.0",
"typecheck:cartridges": "sfcc-ts-typecheck"
}
}If your CI install uses --ignore-scripts, run the synchronization script explicitly before sfcc-ts-typecheck:
pnpm run types:sfcc:syncyarn run types:sfcc:syncnpm run types:sfcc:syncvp run types:sfcc:syncCustom APIs
sfcc-ts-sync-types generates .b2c-script-types/types/sfcc-custom-apis.generated.d.ts from Custom API contracts. It scans every api.json found under cartridge/rest-apis/** and parses the referenced OAS 3.0 schema.yaml files to derive:
SfccCustomApis.Schemas: one entry per named schema incomponents.schemas.SfccCustomApis.Operations: one entry peroperationId, with aHandler,Parameters(grouped bypath/query/header), an optionalRequestBody, and theResponsetype resolved from the first successful (2xx)application/jsonresponse.
Type the endpoint implementation script with the generated operation type:
// @ts-check
const RESTResponseMgr = require("dw/system/RESTResponseMgr")
/** @type {SfccCustomApis.Operations["getLoyaltyInfo"]["Handler"]} */
const getLoyaltyInfo = function () {
const customerId = request.getHttpParameterMap().get("c_customer_id").getStringValue()
/** @type {SfccCustomApis.Operations["getLoyaltyInfo"]["Response"]} */
const info = { tier: "silver", points: 14275 }
return RESTResponseMgr.createSuccess(info).render()
}
getLoyaltyInfo.public = true
exports.getLoyaltyInfo = getLoyaltyInfoNotes:
$refvalues are resolved forcomponents.schemas,components.parameters,components.requestBodies, andcomponents.responses.- Typed
additionalPropertiesschemas are emitted asRecord<string, ValueType>. allOf/oneOf/anyOfcompositions are not resolved yet and fall back tounknown.- Schema and operation names are expected to be unique across all scanned cartridges; on a name collision, the first occurrence found wins.
Custom job steps
sfcc-ts-sync-types generates .b2c-script-types/types/sfcc-job-steps.generated.d.ts from the effective steptypes.json files in cartridge-path order. The generated SfccJobSteps.Definitions registry contains each resolved custom type ID with:
Input: parameters accepted by a metadata-driven test invocation. Optional parameters and parameters with defaults can be omitted.Parameters: the values received by the job module after defaults and type conversion.StatusCode: the built-inOKandERRORcodes plus declared custom task status codes.Functions: the configured task function or chunk lifecycle functions with their SFCC arguments and return types.
Use a generated function signature to type a job module implementation:
// @ts-check
/** @type {SfccJobSteps.Definitions["custom.GenerateFeed"]["Functions"]["run"]} */
const run = function (parameters, stepExecution) {
const mode = parameters.Mode
const jobID = stepExecution.getJobExecution().getJobID()
// ...
}
exports.run = runParameter metadata maps boolean to boolean, long and double to number, and string to string. enum-values become string literal unions. Date and time parameter types follow target-type: long becomes number, while date and the default conversion become Date.
steptypes.json does not describe the items passed through a chunk step. Without additional project information, generated read, process, and write signatures therefore use unknown. Augment SfccJobSteps.ChunkStepTypes in a project declaration file to connect those functions with application-specific types:
export {}
declare global {
namespace SfccJobSteps {
interface ChunkStepTypes {
"custom.SynchroniseWizvilleReviews": {
ReadItem: dw.catalog.Product
ProcessedItem: {
product: dw.catalog.Product
ratingAverage: number
ratingCount: number
}
}
}
}
}The generated function types then make read return ReadItem, pass it to process, and pass ProcessedItem values to write. Both read and process may also return null or undefined according to the chunk lifecycle. If ProcessedItem is omitted, it defaults to ReadItem, which covers chunk steps without a process function.
When the generated declaration is loaded together with @commerce-klaus/vitest-sfcc, loadSfccJobStep() accepts only registered type IDs and its run() method requires the corresponding Input. Without generated declarations, the API retains its general string and Record<string, unknown> fallback.
tsserver Plugin
Add the plugin to your cartridge jsconfig.json or tsconfig.json:
{
"compilerOptions": {
"plugins": [{ "name": "@commerce-klaus/typescript-sfcc" }]
}
}The plugin is additive when the official Salesforce B2C Commerce VS Code extension is installed. Resolutions already supplied by Salesforce's TypeScript Server plugin take precedence. Commerce Klaus only resolves modules left unresolved and continues to load generated project declarations and transform module.superModule. Without the Salesforce extension, the Commerce Klaus resolver remains a standalone fallback.
CLI
The package ships these CLI binaries:
sfcc-ts-typechecksfcc-ts-sync-types
Both commands use oclif, matching the B2C CLI plugin commands for flag parsing, help, colored terminal output, errors, and structured --json output. Run either command with --help to see its complete option reference.
The typecheck loads the project's installed TypeScript compiler, including when it runs through the B2C CLI plugin. This keeps compiler options and diagnostics aligned with the TypeScript version declared by the project.
The Salesforce extension provides editor IntelliSense but does not expose a project-wide TypeScript check. Use sfcc-ts-typecheck locally and in CI to apply the same cartridge-aware checks without depending on an editor extension.
Default behavior (no flags):
- searches from the current working directory upwards for
cartridges/jsconfig.json - if no
referencesare present, it typechecks the given config itself
Basic calls:
pnpm exec sfcc-ts-typecheck
pnpm exec sfcc-ts-typecheck --project cartridges/jsconfig.json
pnpm exec sfcc-ts-typecheck --project cartridges/tsconfig.jsonyarn exec sfcc-ts-typecheck
yarn exec sfcc-ts-typecheck --project cartridges/jsconfig.json
yarn exec sfcc-ts-typecheck --project cartridges/tsconfig.jsonnpm exec -- sfcc-ts-typecheck
npm exec -- sfcc-ts-typecheck --project cartridges/jsconfig.json
npm exec -- sfcc-ts-typecheck --project cartridges/tsconfig.jsonvp exec sfcc-ts-typecheck
vp exec sfcc-ts-typecheck --project cartridges/jsconfig.json
vp exec sfcc-ts-typecheck --project cartridges/tsconfig.jsonIf your project config is outside the cartridges folder, pass the cartridges root explicitly:
pnpm exec sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridgesyarn exec sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridgesnpm exec -- sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridgesvp exec sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridgespackage.json example:
{
"scripts": {
"typecheck:cartridges": "sfcc-ts-typecheck --project cartridges/tsconfig.json"
}
}Exit codes:
0: no diagnostics2: diagnostics found1: runtime/config error (for example missing config file)
Registration validation
When a cartridge package.json declares a hooks path, sfcc-ts-typecheck also validates the referenced hooks.json file:
{
"hooks": "./cartridge/scripts/hooks.json"
}{
"hooks": [{ "name": "dw.order.calculate", "script": "./hooks/calculate" }]
}It reports malformed registrations and missing hook scripts. For Salesforce dw.* hooks, it also reports statically detectable missing CommonJS exports such as exports.afterPOST or module.exports = { afterPOST }.
Project-specific hook names do not necessarily identify an exported method, so their registrations are validated structurally without inferring an export name.
The checker does not maintain or validate against a list of known Salesforce extension point names. The vendored hook declarations under .b2c-script-types/types/dw/**/hooks/*.d.ts only cover a subset of valid dw.* hooks (for example, CalculateHooks does not model dw.order.calculateDiscounts), so an unmodeled hook name is not necessarily invalid.
Registration validation runs as part of sfcc-ts-typecheck only; the tsserver plugin does not validate hooks.json in the editor.
Diagnostics for a specific hooks.json registration (unresolved script, missing export) are anchored to that entry, not to the top of the file, so multiple registration issues in the same hooks.json are reported at their correct positions.
sfcc-ts-sync-types options:
--min-version X.Y.Z: refreshes types if vendored version is older than required--force: always refreshes vendored types--output <path>: optional output path for generatedjsconfigmetadata--site-template-path <path>: optional path to the site template directory (relative to current directory or absolute). The tool reads metadata from<path>/meta/*.xml.
Both standalone commands also accept --project-directory <path> (alias --working-directory) and --json. Colors are automatically disabled when the terminal does not support them.
Clean generated declarations programmatically
cleanGeneratedTypes() removes only the project-specific declarations managed by Commerce Klaus. Salesforce Script API types and unrelated declaration files in .b2c-script-types/types remain untouched:
import { cleanGeneratedTypes } from "@commerce-klaus/typescript-sfcc"
const preview = cleanGeneratedTypes({
currentDirectory: process.cwd(),
dryRun: true,
})Each result entry distinguishes a file that exists from one that was actually removed, so the same API works for previews and cleanup automation.