Skip to content

NPM version Downloads

@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, and module.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:

AreaCommerce Klaus additionPractical benefit
Project contractsProjects metadata, hooks.json, Custom API schemas, and steptypes.json into TypeScript declarationsConfiguration and JavaScript implementation are checked together
Super modulesResolves and types module.superModule chainsCartridge overrides remain navigable and type-safe
Type checkingRuns the cartridge-aware TypeScript compiler from the command lineThe same class of diagnostics can block CI without a VS Code extension
Registration validationChecks hook descriptors, referenced scripts, and required CommonJS exportsBroken registrations fail before deployment
Shared toolingReuses the same cartridge semantics across TypeScript, ESLint, Vite, Babel, and Vitest packagesLocal 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.json and Salesforce Script API declarations define hook boundaries
  • Custom API schemas define request and response shapes
  • steptypes.json defines 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:

js
/** @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 ​

bash
pnpm add -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-cli
bash
yarn add -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-cli
bash
npm install -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-cli
bash
vp install -D @commerce-klaus/typescript-sfcc typescript @salesforce/b2c-cli

SFCC 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.json
  • cartridges/<cartridge>/jsconfig.json

Example workflow with the Salesforce B2C Developer Tooling CLI:

bash
b2c setup ide vscode-types --copy --force --output .b2c-script-types/jsconfig.generated.json

Notes:

  • sfcc-dts is not required for dw/* resolution in this package.
  • Keep .b2c-script-types/types available in the workspace before running sfcc-ts-typecheck.

Custom Attributes ​

  • sfcc-ts-sync-types extends b2c-script-types by 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 example CustomObjectExampleNotificationCustomAttributes and ProductCustomAttributes.
  • The generated declarations are consumed by both sfcc-ts-typecheck and the tsserver plugin, so editor diagnostics and CLI diagnostics use the same custom attribute typing.
  • The generated declarations also patch dw/object/SystemObjectMgr so supported type literals narrow getAllSystemObjects, querySystemObject, and querySystemObjects to the matching system object class.
  • Custom attribute values are inferred by metadata type. For enums, select-multiple-flag=true is treated as a multi-value enum and emitted as SfccEnumValue<T>[].
  • Attribute type values follow Salesforce metadata schema (metadata.xsd), where enum multi-select is modeled via enum-of-* plus select-multiple-flag=true (not via set-of-enum-of-*).

Custom attribute mapping reference:

Metadata typeGenerated TypeScript type
booleanboolean
date, datetimeDate
double, int, integer, long, number, quantitynumber
email, password, string, text, urlstring
htmldw.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=trueSfccEnumValue<...>[]
enum-of-* (without value-definitions)SfccEnumValue<baseType>
unknown enum-of-*SfccEnumValue<string | number>
set-of-stringstring[]
set-of-int, set-of-doublenumber[]
invalid set-of-enum-of-* (not in metadata.xsd)unknown[]
unknown set-of-*unknown[]
unsupported/unknown single-value typeunknown

Example (Product.custom):

product.ts
ts
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:

notification.js
js
// @ts-check

/**
 * @param {CustomObjectExampleNotificationCustomAttributes} custom
 * @returns {string | undefined}
 */
function readNotification(custom) {
  return custom.eventCode
}

System Object attributes use the same pattern:

product.js
js
// @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:
    • type
    • value-definitions and value-definition/value
    • select-multiple-flag (for enum multi-select)
    • mandatory-flag (required vs optional property)
  • Supported metadata roots are type-extension/custom-attribute-definitions and custom-type/attribute-definitions.
  • type-extension object types are assigned to dw/* modules via a preferred mapping table for known SFCC system objects (for example Product -> 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 to unknown[].

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:

calculate.js
js
// @ts-check

/** @type {SfccHooks.OrderCalculate} */
const calculate = function (lineItemCtnr) {
  // lineItemCtnr is typed from dw/order/hooks/CalculateHooks
}

exports.calculate = calculate

SfccHooks 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:

cartridges/sfcc-hooks.d.ts
ts
export {}

declare global {
  namespace SfccHooks {
    type ShopperProductModifyGetResponse = (document: { c_brand: string }) => void
  }
}

Use the project-local alias from the hook implementation:

modifyGETResponse.js
js
// @ts-check

/** @type {SfccHooks.ShopperProductModifyGetResponse} */
const modifyGETResponse = function (document) {
  document.c_brand = "Example"
}

exports.modifyGETResponse = modifyGETResponse

Types 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 cartridges
  • cartridges/<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:

package.json
json
{
  "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:

bash
pnpm run types:sfcc:sync
bash
yarn run types:sfcc:sync
bash
npm run types:sfcc:sync
bash
vp run types:sfcc:sync

Custom 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 in components.schemas.
  • SfccCustomApis.Operations: one entry per operationId, with a Handler, Parameters (grouped by path/query/header), an optional RequestBody, and the Response type resolved from the first successful (2xx) application/json response.

Type the endpoint implementation script with the generated operation type:

script.js
js
// @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 = getLoyaltyInfo

Notes:

  • $ref values are resolved for components.schemas, components.parameters, components.requestBodies, and components.responses.
  • Typed additionalProperties schemas are emitted as Record<string, ValueType>.
  • allOf/oneOf/anyOf compositions are not resolved yet and fall back to unknown.
  • 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-in OK and ERROR codes 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:

js
// @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 = run

Parameter 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:

cartridges/sfcc-job-steps.d.ts
ts
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:

cartridges/jsconfig.json
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-typecheck
  • sfcc-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 references are present, it typechecks the given config itself

Basic calls:

bash
pnpm exec sfcc-ts-typecheck
pnpm exec sfcc-ts-typecheck --project cartridges/jsconfig.json
pnpm exec sfcc-ts-typecheck --project cartridges/tsconfig.json
bash
yarn exec sfcc-ts-typecheck
yarn exec sfcc-ts-typecheck --project cartridges/jsconfig.json
yarn exec sfcc-ts-typecheck --project cartridges/tsconfig.json
bash
npm exec -- sfcc-ts-typecheck
npm exec -- sfcc-ts-typecheck --project cartridges/jsconfig.json
npm exec -- sfcc-ts-typecheck --project cartridges/tsconfig.json
bash
vp exec sfcc-ts-typecheck
vp exec sfcc-ts-typecheck --project cartridges/jsconfig.json
vp exec sfcc-ts-typecheck --project cartridges/tsconfig.json

If your project config is outside the cartridges folder, pass the cartridges root explicitly:

bash
pnpm exec sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridges
bash
yarn exec sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridges
bash
npm exec -- sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridges
bash
vp exec sfcc-ts-typecheck --project config/tsconfig.cartridges.json --cartridges-dir cartridges

package.json example:

package.json
json
{
  "scripts": {
    "typecheck:cartridges": "sfcc-ts-typecheck --project cartridges/tsconfig.json"
  }
}

Exit codes:

  • 0: no diagnostics
  • 2: diagnostics found
  • 1: 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:

cartridges/app_custom/package.json
json
{
  "hooks": "./cartridge/scripts/hooks.json"
}
cartridges/app_custom/cartridge/scripts/hooks.json
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 generated jsconfig metadata
  • --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:

ts
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.

Released under the MIT License.