Modernizing Steem.js — Day 5: Shipping First-Class TypeScript Types
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.tstypes 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
- 🛠️ Code (fork): https://github.com/blazeapps007/steem-js (
BlazeDevelopmentbranch) - 📖 Documentation: https://blazeapps007.github.io/steem-js/
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