WDK JSDoc & Type Annotation Review
Review the source files at the provided path (or the current project's src/ directory if none given). Apply all rules below.
For each violation found, report:
- Rule violated (e.g., R1, R2, TD1, etc.)
- File and line
- The offending code
- Suggested fix with corrected code
Group findings by file. After all files are reviewed, provide a summary with total violation counts per rule.
WDK Types & JSDoc Standards
You are reviewing source files for a WDK (Wallet Development Kit) module. Apply the rules below to identify JSDoc and type-annotation violations and suggest fixes.
The rules are repository-agnostic. No WDK module is a "golden reference" — apply every rule to every module equally. If existing code conflicts with a rule, the code is the candidate for change, not the rule.
JSDoc Format Rules
R1. Every JSDoc block must start with a plain-text description before any tags
Tags like @param, @returns, @type must not appear before the description.
Violation:
/**
* @param {Provider} provider - An ethers provider.
* @returns {Promise<string>} The address.
*/
Correct:
/**
* Returns the wallet address from the given provider.
*
* @param {Provider} provider - An ethers provider.
* @returns {Promise<string>} The address.
*/
R2. Use {Object} (capitalized) in @typedef kind-markers and @template constraints, not {object}
/** @typedef {object} GetBalanceResult */ // wrong — use Object as the kind-marker
/** @typedef {Object} GetBalanceResult */ // correct
/** @template {object} TSchema */ // wrong
/** @template {Object} TSchema */ // correct — consistent with all other WDK modules
This applies only to @typedef kind-markers and @template constraints. Using {Object} as a value type in @param, @property, @returns, or @type is forbidden — see R13 (the single-use options-bag pattern in R25 is the one exception).
R3. Dash separator required in @property and @param
Every @param and @property tag must have a dash between the name and the description.
@param {string} address The wallet address. // wrong
@param {string} address - The wallet address. // correct
R4. No [param=value] default syntax — state default in description text
@param {number} [timeout=15000] - Connection timeout. // wrong
@param {number} [timeout] - Connection timeout (default: 15,000). // correct
The ban covers @template too — declare generic type parameters with the brace form, never bracket-defaults:
@template [TSignedTransaction=unknown] // wrong
@template {unknown} TSignedTransaction // correct
R5. @private only on class/namespace members, not module-level constants
Module-level constants (even those prefixed with _) shouldn't use @private.
Violation:
/**
* Compiled multicall constructor bytecode.
* @private
* @type {string}
*/
const _MULTICALL_BYTECODE = ...
Drop @private; keep the description and @type.
R6. Private members get only /** @private */ — no full JSDoc
Private methods and properties (prefixed with _) get a minimal block. Don't document parameters, return types, descriptions, or @type for private members.
Violation:
/**
* Returns the fee estimator for the given bundler URL.
* @private
* @param {string} bundlerUrl - The bundler URL.
* @returns {Promise<FeeEstimator>} The fee estimator.
*/
async _getFeeEstimator (bundlerUrl) { ... }
Correct:
/** @private */
async _getFeeEstimator (bundlerUrl) { ... }
Same for private properties: /** @private */ only, nothing else. Internal (@internal) constants and properties likewise don't need full documentation. And never use company- or team-internal jargon (e.g., "Phase 1") anywhere in documentation — every term must make sense outside the organization.
R7. @protected members need full documentation
Properties need description + @protected + @type. Methods need a full block: description, @protected, @param for every parameter, and @returns.
Violation (property — missing description):
/** @protected @type {number} */
this._timeout = timeout
Correct (property):
/**
* Connection timeout in milliseconds.
* @protected
* @type {number}
*/
this._timeout = timeout
Correct (method):
/**
* Builds the Safe initialization data for the given config.
*
* @protected
* @param {SafeConfig} config - The Safe construction config.
* @returns {Promise<string>} The encoded initialization data.
*/
async _buildInitData (config) { ... }
R8. Always provide full JSDoc on overridden methods
Copy the parent's JSDoc on the override. Keep the superclass description and only append implementation-specific details after it (e.g., "Establishes the connection to the server. Blockbook is a stateless REST API, so clients don't need to call this method.") — never replace the original description. tsc needs JSDoc on the override for accurate .d.ts generation. Make documentation more blockchain-specific when possible — add @example, narrow return types, clarify behavior.
/**
* Returns the wallet account at a specific index (BIP-44).
*
* @example
* const account = await wallet.getAccount(1); // m/44'/60'/0'/0/1
* @param {number} [index] - The index of the account (default: 0).
* @returns {Promise<WalletAccountEvm>} The account.
*/
async getAccount (index) { ... }
R9. Use @implements for interfaces, @extends only for real inheritance
@implements {IFoo} for interface conformance; @extends only for actual class inheritance. Omit @extends when the extends keyword is already present — it's redundant.
Violation:
/** @abstract @extends IElectrumClient */
export default class BaseClient extends IElectrumClient { ... }
Correct:
/** @abstract @implements {IElectrumClient} */
export default class BaseClient { ... }
When the interface is generic, @implements must supply the concrete type argument, never the bare name:
/** @implements {IWalletAccount} */ // wrong — missing generic argument
/** @implements {IWalletAccount<Cell>} */ // correct
Type Import/Export Rules
R10. Import types via single-line @typedef; import from index.js when available
Don't use runtime import statements for type-only imports.
Violation:
// eslint-disable-next-line no-unused-vars
import BaseClient from './transports/client/base-client.js'
Correct:
/** @typedef {import('./transports/index.js').IElectrumClient} IElectrumClient */
Import SDK-originating types directly from the SDK package, never through local module files that merely re-export them:
/** @typedef {import('./wallet-account-read-only-evm.js').Authorization} Authorization */ // wrong
/** @typedef {import('ethers').Authorization} Authorization */ // correct
R11. Use short alias names in JSDoc, not full import(...) paths inline
Define a @typedef alias at the top of the file; use the short name in @param/@returns.
Violation:
/** @param {import('./transports/mempool-electrum-client.js').MempoolElectrumConfig} config */
Correct:
/** @typedef {import('./transports/index.js').MempoolElectrumConfig} MempoolElectrumConfig */
// later: @param {MempoolElectrumConfig} config
Type Annotation Rules
R12. Canonical type syntax: x[] not Array<x>; Record<string, T> not index signatures
@returns {Promise<Array<ElectrumUtxo>>} // wrong
@returns {Promise<ElectrumUtxo[]>} // correct
@type {{ [name: string]: ISigner }} // wrong
@type {Record<string, ISigner>} // correct
Define @typedef types as the singular element type and apply [] at the usage site (@param {BaseAssetFilter<T>[]} filter) — don't bake array-ness into the typedef.
R13. Type-precision ladder: specific @typedef > SDK type > Record<string, unknown> > (never bare Object)
When choosing a type for a property, parameter, or return value, walk this ladder top-to-bottom and stop at the first option that fits:
- A specific named
@typedef— preferred whenever the shape is known. - An SDK type imported via
@typedef— when the SDK already publishes the type for the concept. Record<string, unknown>— only when the shape is genuinely arbitrary (user-provided metadata blob).- Bare
Object— forbidden as a value type in@param,@property,@returns,@type. (@typedef {Object} Foois fine — that's the typedef-kind marker, see R2.)
If you write {Object} outside a @typedef declaration, you've taken a shortcut. Find the right type.
Two refinements at the extremes of the ladder:
- If even
Pick<>of an existing type leaves a field union looser than the runtime contract (e.g.,number | bigintwhere the method always producesbigint), define and export a new ad-hoc typedef with the exact types. - If a parameter's shape is genuinely implementation-specific and unknowable at the interface level, type it
{unknown}— don't borrow a semantically wrong concrete type just because it's importable.
Violation:
@param {Object} txs - The transactions.
@returns {Promise<Object>} The receipt.
Correct:
/** @typedef {import('abstractionkit').MetaTransaction} MetaTransaction */
/** @typedef {import('abstractionkit').UserOperationReceiptResult} UserOperationReceipt */
@param {MetaTransaction[]} txs - The transactions to send.
@returns {Promise<UserOperationReceipt>} The receipt.
Record<string, unknown> for known shapes is also wrong:
@property {Record<string, unknown>} domain // wrong (known shape)
@property {TypedDataDomain} domain // correct
R14. Use SDK types instead of redundant custom @typedef
When the SDK provides a type for a concept, reference it directly instead of redefining a local equivalent.
@property {BaseClient} [client] // wrong (custom)
@property {IElectrumClient} [client] // correct (SDK type)
R15. Create type aliases for SDK types to match module naming
When using types from external SDKs, create a @typedef alias matching the module's naming (Transfer → SparkTransfer).
Violation:
@returns {Promise<Object | null>} The Spark transfer, or null if not found.
Correct:
/** @typedef {import('@buildonspark/spark-sdk').Transfer} SparkTransfer */
@returns {Promise<SparkTransfer | null>} The Spark transfer, or null if not found.
R16. @returns must include | null when method can return null
If the implementation can return null, the @returns type must explicitly include | null.
@returns {Promise<SparkTransfer>} // wrong (body returns transfer ?? null)
@returns {Promise<SparkTransfer | null>} // correct
R17. Use @throws for error conditions — one tag per distinct condition
Document errors with @throws. Use the specific custom error class, not generic Error. Give each distinct failure condition its own @throws tag with its own class — never merge conditions into one generic tag.
@throws {Error} If the configuration is invalid. // wrong
@throws {ConfigurationError} If the configuration has missing required fields. // correct
@throws {Error} If no signer exists with that name or the signer doesn't support derivation. // wrong — merged
@throws {Error} If a signer name is given but no signer exists with that name. // correct
@throws {SignerError} If the signer doesn't support account derivation. // correct
R18. Use @see for external documentation URLs
Link external protocol specs, RFCs, or API docs with @see.
@see https://electrum.readthedocs.io/en/latest/protocol.html#blockchain-address-get-balance
R19. Use @abstract for abstract methods, @interface for interface classes
/** @interface */
export default class IElectrumClient {
/**
* Connects to the Electrum server.
*
* @abstract
* @returns {Promise<void>}
*/
async connect () { throw new Error('Not implemented') }
}
R20. Discriminated union types for mutually exclusive config options
When a config object has mutually exclusive options (paymaster token vs. sponsorship policy), model them as a discriminated union, not all properties on one type.
/** @typedef {Object} TokenConfig
* @property {string} paymasterTokenAddress */
/** @typedef {Object} SponsorshipConfig
* @property {string} sponsorshipPolicyId */
/** @typedef {Partial<TokenConfig | SponsorshipConfig>} Config */
R21. No blank line between */ and the declaration it documents
/**
* Verifies a message's signature.
*/
// wrong (blank line)
async verify (message, sig) { ... }
/**
* Verifies a message's signature.
*/
async verify (message, sig) { ... } // correct
R22. Define @typedef for structured return types
When a method returns an object with multiple properties, define a named @typedef instead of an inline anonymous type.
@returns {Promise<{confirmed: bigint, unconfirmed: bigint}>} // wrong
/** @typedef {Object} GetBalanceResult
* @property {bigint} confirmed - Confirmed balance in satoshis.
* @property {bigint} unconfirmed - Unconfirmed balance in satoshis. */
@returns {Promise<GetBalanceResult>}
R23. Ensure .d.ts types match runtime types
Generated .d.ts types must reflect runtime behavior. If a value is bigint at runtime, the .d.ts must not declare it as number. When a third-party value's declared type resolves to any, verify its actual runtime type before writing it into JSDoc or .d.ts — don't guess.
R24. Remove unused @typedef imports
If a @typedef import is no longer referenced in the file, remove it. Same for types re-exported from .d.ts that aren't part of the public API.
R25. Use named @typedef for inline object types in @type annotations
When a class property's @type uses an inline object type with multiple properties, extract it into a top-level @typedef. This mirrors R22 but applies to @type on class members.
Violation:
/**
* Cached viem clients.
* @protected
* @type {{ publicClient: PublicClient, bundlerClient: BundlerClient } | undefined}
*/
this._viemClients = undefined
Define ViemClients as a typedef and reference it by name.
Boundaries of the named-typedef rule:
- Never use inline object literals anywhere — including
.d.tsgeneric arguments:extends WdkBaseAssetRegistry<{ id: string; ... }>→extends WdkBaseAssetRegistry<TokenAsset>. - Alias primitive unions repeated across many fields:
/** @typedef {string | number} Blockchain */. - But do not create a typedef for a single-use options parameter. Document it inline —
@param {Object} [options] - The transaction's options.plus dotted@param {boolean} [options.isActivation] - ...per property (or dotted@property {Object} [content]+@property {boolean} [content.confirmed]entries inside a typedef) — so every field still gets a description. This is the one sanctioned use of{Object}as a value type (see R13).
R26. undefined vs null convention
undefined— uninitialized optional/cached properties, and absent optional values (passundefined, never an empty{}placeholder).null— lookup results when nothing is found (T | null, neverT | undefined), and key-material properties cleared bydispose()(type themType | nullper the base-class disposal contract).
/** @protected @type {SomeType | null} */
this._cachedValue = null // wrong — uninitialized cache is undefined
@returns {T | undefined} The matching asset, or `undefined` if no asset matches. // wrong
@returns {T | null} The matching asset, or `null` if no asset matches the id. // correct
const providerOpts = config.chainId ? { staticNetwork: true } : {} // wrong — use undefined, not {}
R27. Overloads: one @overload block per mode, with overload-specific names and @throws
When a method has @overload JSDoc, each overload's @param name should reflect the specific type for that overload — not the implementation's generic union name.
Violation:
/** @overload @param {string | Uint8Array} seedOrAccount - The seed phrase. */
/** @overload @param {WalletAccountEvm} seedOrAccount - An existing account. */
Correct:
/** @overload @param {string | Uint8Array} seed - The seed phrase. */
/** @overload @param {WalletAccountEvm} account - An existing account. */
Document distinct construction/usage modes as separate @overload blocks in the first place — never a single union-typed parameter ({string | Uint8Array | ISigner} seedOrSigner), even when the implementation is shared. Keep each overload's @throws specific to that overload: drop discriminator clauses ("...and index is a number") already implied by its signature.
R28. Every documented field needs a meaningful description
@param, @property, @returns, @throws, and member descriptions must contain a non-empty, useful sentence that adds information beyond the type and name. Empty descriptions, single-word descriptions that just restate the parameter name, and "the X" tautologies are forbidden.
Descriptions must also be behaviorally accurate: state how multiple filter conditions combine (AND vs OR); respect ownership semantics (dispose closes only internal connections when clients can be injected — say so); never imply an outcome that doesn't hold in every case (e.g., "Omit to use the default signer" when omission also works without one). Every @returns tag carries a meaningful text description alongside the type — including on @protected methods returning void.
If a default exists, include it per R4.
For @protected methods (part of the extension surface — see AD5), this rule applies fully — protected ≈ public for documentation purposes. Private members under R6 are the only exception.
Violation (no description):
@property {string} projectName
@property {boolean} [enabled]
Violation (tautology):
@param {Config} config - The config.
@returns {Account} The account.
Correct:
@property {string} projectName - Free-form identifier appended to every UserOperation callData. Max 50 bytes.
@property {boolean} [enabled] - Whether the identifier is appended (default: true).
R29. No implementation-detail prefixes in public type names
Type names in the public API describe what the type represents, not how it's used. Prefixes like Cached*, Internal*, Tmp*, Resolved*, Pending* describe lifecycle/usage state — those go in code comments or in a @property description, not the type name.
/** @typedef {Object} CachedTransactionQuote ... */ // wrong
/** @typedef {Object} TransactionQuote ... */ // correct
If the cache-vs-fresh distinction matters at the type level (rare), encode it as @property {boolean} fromCache rather than baking it into the name.
R30. Compound type names use SDK-aligned word-break casing
When a type name is built from a multi-word technical term that has established casing in the relevant SDK or specification, match that casing exactly.
| Term | Wrong | Correct |
|---|---|---|
| On-chain identifier | OnchainIdentifier |
OnChainIdentifier |
| User operation | Useroperation |
UserOperation |
| Bundler URL | Bundlerurl |
BundlerUrl |
| Typed data | Typeddata |
TypedData |
| Multi-chain | Multichain |
MultiChain |
This applies equally to property and parameter names — onChainIdentifier, not onchainIdentifier. When in doubt, search the SDK's published types for the exact casing.
R31. Use Pick<Config, ...>, not Partial<Config>, when the function only needs specific fields
Partial<Config> says "any subset, including the empty object" — right for override parameters where the caller may supply zero or more fields. Wrong for a method that requires a specific subset.
If a method uses three of Config's eight properties, declare exactly those three: Pick<Config, 'a' | 'b' | 'c'>. Documents the dependency at the type level; prevents callers from passing unrelated fields.
Violation:
/** @protected
* @param {Partial<EvmErc4337Config>} config - The configuration. */
async _createSafeAccount (config) {
const v = config.safeModulesVersion ?? '0.3.0'
const e = config.entryPointAddress
const i = config.onchainIdentifier
// only these three are read
}
Correct:
/** @protected
* @param {Pick<EvmErc4337Config, 'safeModulesVersion' | 'entryPointAddress' | 'onchainIdentifier'>} config -
* The Safe-construction subset of the wallet config: module version, entry-point address, and optional on-chain identifier. */
async _createSafeAccount (config) { ... }
When the same Pick is used in multiple places, lift it to a named typedef.
The same discipline applies to override parameters: a per-call config-override is Partial<...> of the specific overridable subset — e.g., Partial<SponsorshipConfig | PaymasterTokenConfig> — not Omit<Config, 'transferMaxFee'> of the full config. Pick the utility type that expresses exactly what may be overridden.
R32. Descriptions state what the component is or does — nothing else
No design rationale, backwards-compatibility or historical notes, meta-commentary on how to access members, restating what a tag already conveys (@internal means "not public API" — don't repeat it), or listing fields documented by their own @property/@param tags. Behavioral prose belongs on the methods exhibiting the behavior, not on @typedef declarations. Error-class descriptions say what the error represents, not when it is thrown.
Violation:
/**
* A minimal, cross-chain signer interface. Chain-specific signers can extend
* this contract with additional capabilities (e.g., signTransaction, signPsbt),
* which are intentionally kept out of the base to remain chain-agnostic.
*/
Correct:
/**
* A minimal, cross-chain signer interface.
*/
R33. Flexible input types for numerics and chain identifiers
User-facing numeric inputs (amounts, fees, limits) must accept number | bigint, and blockchain chain identifiers must accept both string and numeric forms, so callers can pass any valid representation. This applies to inputs only — outputs keep the exact runtime type (R23).
Violation:
@property {bigint} [swidgeMaxFee] - The maximum total fee for swidge operations.
@property {string} chainId - The chain the asset lives on.
Correct:
@property {number | bigint} [swidgeMaxFee] - The maximum total fee for swidge operations.
@property {string | number} chainId - The chain the asset lives on.
R34. Brackets for omittable params; {T | undefined} for non-omittable ones
Mark trailing optional arguments with brackets ([param]), never a | undefined union. Conversely, a possibly-undefined positional parameter followed by a required parameter can never be omitted at the call site — type it {T | undefined}, not [param].
Violation:
@param {string | string[] | undefined} wallet - The wallet names. // trailing optional — use brackets
// _isActivatingTransfer (type, value) — value is required
@param {string} [type] - The transaction's contract type. // cannot be omitted — not optional
Correct:
@param {string | string[]} [wallet] - The wallet names.
@param {string | undefined} type - The transaction's contract type.
R35. Don't use @deprecated to mask a breaking type change
If methods no longer accept a type, marking the old type @deprecated doesn't avoid the break — the methods already reject it. Either add overloads that genuinely still accept the old type, or remove the unsupported type entirely (including its re-exports) and take the break honestly.
Review Workflow
- JSDoc structure — every block starts with a description (R1);
{Object}not{object}(R2); no blank line before declaration (R21). - Tag formatting — dash separators in
@param/@property(R3); defaults in description text (R4). - Visibility —
@privateonly on class members (R5); private members get minimal JSDoc (R6);@protectedhas full annotation (R7). - Inheritance — overrides have full JSDoc (R8);
@implementsvs@extends(R9);@abstract/@interface(R19). - Imports —
@typedef {import(...)}not runtime imports (R10); short aliases not inline paths (R11); no unused imports (R24). - Type annotations — canonical type syntax (R12); precision ladder (R13); SDK types preferred (R14, R15); discriminated unions (R20); typedef for structured returns (R22); typedef for inline
@typeon properties (R25);undefinednotnullfor cached props (R26);Pick<Config>notPartial<Config>(R31); flexible numeric/chain-id inputs (R33); brackets vs| undefinedon positional params (R34). - Completeness — meaningful, behaviorally accurate descriptions on every documented field (R28); concise, purposeful descriptions (R32);
| nullwhen applicable (R16); one@throwsper condition (R17);@seefor docs (R18). - Naming — no impl-detail prefixes (R29); SDK-aligned casing (R30); overload-specific param names (R27).
The
.d.tsmaintenance rules (TD1–TD9) live in the companion skill wdk-review-dts; run it whenever a change touchestypes/.