Modernizing Steem.js — Day 5: Shipping First-Class TypeScript Types

in Steem Dev2 days ago

Part 5 of my 7-part Steem.js modernization series. The library now runs everywhere and stays byte-compatible. Today is about developer experience: shipping .d.ts types so editors give you autocomplete and type-checking out of the box.

The challenge: the API doesn't exist as written code

Here's the twist that makes typing Steem.js interesting. Most of its surface isn't hand-written. The Steem class generates a method for every entry in src/api/methods.js at construction time:

// For each method `foo`, the constructor creates:
//   foo, fooWith, fooAsync, fooWithAsync
methods.forEach((m) => {
  const name = camelCase(m.method);
  this[name] = (...args) => this.send(m.api, { method: m.method, params }, cb);
  this[`${name}Async`] = promisify(this[name]);
  // …and the `With` variants
});

The same trick generates a broadcast helper for every entry in operations.js. That's elegant for maintenance — but a TypeScript compiler pointed at the source sees none of those ~108 API methods or ~67 broadcast operations. There's nothing concrete to infer.

The solution: generate types from the same descriptors

The methods are data-driven, so the types should be too. scripts/gen-types.js reads the same methods.js / operations.js descriptor arrays that drive the runtime and emits a matching dist/index.d.ts:

export interface SteemApi {
  getAccounts(names: string[], callback: Callback): void;
  getAccountsAsync(names: string[]): Promise<any>;
  getAccountsWith(options: { names: string[] }, callback: Callback): void;
  getAccountsWithAsync(options: { names: string[] }): Promise<any>;
  // …all 108 methods × 4 call styles, generated
}

export interface SteemBroadcast {
  vote(wif: string, voter: string, author: string,
       permlink: string, weight: number, callback: Callback): void;
  voteAsync(wif: string, voter: string, author: string,
            permlink: string, weight: number): Promise<any>;
  // …every operation, generated
}

export interface SteemAuth { /* generateKeys, toWif, signTransaction, … */ }

Because the generator is driven by the exact same source of truth as the runtime, the types can never drift from the actual methods. Add a method to methods.js, run npm run gen-types, and the type appears automatically.

Wired into the build and the package

npm run build runs tsup and then gen-types, so the published package always carries fresh declarations. The exports map points types at them first:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.mjs",
    "require": "./dist/index.js"
  }
}

What you get as a consumer

import steem from '@steemit/steem-js';

const [account] = await steem.api.getAccountsAsync(['blaze.apps']);
//      ^ typed                  ^ autocompletes, checks args

Full editor autocomplete, inline signatures, and compile-time argument checking — for an API that is still generated dynamically at runtime. Source stays plain JavaScript; types ship alongside.

Tomorrow: the final engineering phase — cross-runtime verification, CI matrix, and 100% generated documentation.

Links

Support Secure Steem Development

If you value proactive engineering, UX polish, and performance optimizations for the STEEM ecosystem, please consider supporting my witness: blaze.apps

🗳️ Vote Here:
Vote for blaze.apps Witness

Coin Marketplace

STEEM 0.05
TRX 0.32
JST 0.082
BTC 65690.94
ETH 1793.47
USDT 1.00
SBD 0.42