From 4dcce85307119f2032404cbd5f1a4635e635c00e Mon Sep 17 00:00:00 2001 From: coderwhy Date: Sat, 22 Aug 2026 09:51:50 +0800 Subject: [PATCH] feat: add TypeScript declarations --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 9 +++- README.md | 26 ++++++++-- README.zh-CN.md | 21 +++++++- ROADMAP.md | 2 +- Test/types.test.ts | 57 +++++++++++++++++++++ index.d.ts | 106 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 19 ++++++- package.json | 10 +++- 9 files changed, 240 insertions(+), 11 deletions(-) create mode 100644 Test/types.test.ts create mode 100644 index.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 181b5ea..58477b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,5 @@ jobs: cache: npm - run: npm ci - run: npm test + - run: npm run test:types - run: npm pack --dry-run diff --git a/CHANGELOG.md b/CHANGELOG.md index 7401543..dc2739f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,14 @@ All notable changes to `hy-event-store` are documented in this file. -## 1.4.0 (unreleased) +## Unreleased + +### Added + +- Add first-party TypeScript declarations for event payloads, state keys, + actions, and action return values, plus compile-time regression coverage. + +## 1.4.0 - 2026-08-19 ### Added diff --git a/README.md b/README.md index 73b7816..eaf1083 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,30 @@ Mini Program build workflows. npm install hy-event-store ~~~ -The package currently exposes a CommonJS entry point: +The package exposes a CommonJS entry point and ships first-party TypeScript +declarations: ~~~js const { HYEventBus, HYEventStore } = require("hy-event-store") ~~~ +TypeScript users can add event and state types without changing the runtime API: + +~~~ts +const bus = new HYEventBus<{ ready: []; signedIn: [user: { name: string }] }>() +bus.on("signedIn", user => console.log(user.name)) + +const store = new HYEventStore({ + state: { count: 0 }, + actions: { + increment(state, amount: number) { + state.count += amount + return state.count + } + } +}) +~~~ + ## Quick start ### Event bus @@ -181,9 +199,9 @@ The distributed package is CommonJS and has no runtime dependencies. CI validates the package on Node.js 18 through 26. Browser and Mini Program projects should use their normal npm-package build or bundling workflow. -Native ESM exports, TypeScript declarations, a read-only state selector, and -an opt-in full snapshot mode for onStates are planned design -improvements; see the [roadmap](ROADMAP.md). +Native ESM exports, a read-only state selector, and an opt-in full snapshot +mode for onStates are planned design improvements; see the +[roadmap](ROADMAP.md). ## Development diff --git a/README.zh-CN.md b/README.zh-CN.md index 4a3e50d..d66e3fb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -22,12 +22,29 @@ npm install hy-event-store ~~~ -当前包提供 CommonJS 入口: +当前包提供 CommonJS 入口,并内置第一方 TypeScript 声明: ~~~js const { HYEventBus, HYEventStore } = require("hy-event-store") ~~~ +TypeScript 用户无需改变运行时 API,即可补充事件和状态类型: + +~~~ts +const bus = new HYEventBus<{ ready: []; signedIn: [user: { name: string }] }>() +bus.on("signedIn", user => console.log(user.name)) + +const store = new HYEventStore({ + state: { count: 0 }, + actions: { + increment(state, amount: number) { + state.count += amount + return state.count + } + } +}) +~~~ + ## 快速开始 ### 事件总线 @@ -164,7 +181,7 @@ store.offStates(["count", "status"], renderProfile) 发布包为 CommonJS,且没有运行时依赖。CI 在 Node.js 18 至 26 上验证。浏览器和小程序项目应通过各自的 npm 包构建或打包流程使用本包。 -原生 ESM 导出、TypeScript 声明、只读状态选择器与 onStates 的完整快照模式属于后续设计项,详见[路线图](ROADMAP.md)。 +原生 ESM 导出、只读状态选择器与 onStates 的完整快照模式属于后续设计项,详见[路线图](ROADMAP.md)。 ## 开发 diff --git a/ROADMAP.md b/ROADMAP.md index ac7a5ad..8a84108 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -21,7 +21,7 @@ keeping the public API compatible. These items need API design or compatibility research before implementation: -- Add first-party TypeScript declarations +- Add first-party TypeScript declarations and compile-time coverage ([#25](https://github.com/coderwhy/hy-event-store/issues/25)). - Document and test CommonJS, ESM, and Mini Program import paths ([#27](https://github.com/coderwhy/hy-event-store/issues/27)). diff --git a/Test/types.test.ts b/Test/types.test.ts new file mode 100644 index 0000000..3dba997 --- /dev/null +++ b/Test/types.test.ts @@ -0,0 +1,57 @@ +import { HYEventBus, HYEventStore } from ".." + +type AppEvents = { + ready: [] + signedIn: [user: { id: string; name: string }] +} + +const bus = new HYEventBus() + +bus.on("signedIn", user => { + user.id.toUpperCase() + user.name.toUpperCase() +}) +bus.emit("ready") +bus.emit("signedIn", { id: "1", name: "coderwhy" }) + +// @ts-expect-error An event payload must match its declared event tuple. +bus.emit("signedIn", "coderwhy") + +const store = new HYEventStore({ + state: { + count: 0, + status: "idle" as "idle" | "ready" + }, + actions: { + increment(state, amount: number) { + state.count += amount + return state.count + }, + async load(state, label: string) { + state.status = "ready" + return `${label}:${state.status}` + } + } +}) + +store.onState("count", count => { + count.toFixed() +}) + +store.onStates(["count", "status"], change => { + change.count?.toFixed() + change.status?.toUpperCase() +}) + +store.setState("count", 1) +store.setState("status", "ready") + +const nextCount: number = store.dispatch("increment", 2) +const loaded: Promise = store.dispatch("load", "home") + +// @ts-expect-error State keys must exist in the declared state object. +store.setState("missing", 1) +// @ts-expect-error State values must match their declared key type. +store.setState("count", "1") +// @ts-expect-error Action arguments are inferred from the action signature. +store.dispatch("increment", "2") diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..0f4f5ca --- /dev/null +++ b/index.d.ts @@ -0,0 +1,106 @@ +export type EventMap = Record; + +export type EventCallback = ( + ...payload: Payload +) => void; + +export class HYEventBus { + on>( + eventName: EventName, + eventCallback: EventCallback, + thisArg?: unknown + ): this; + + once>( + eventName: EventName, + eventCallback: EventCallback, + thisArg?: unknown + ): this; + + emit>( + eventName: EventName, + ...payload: Events[EventName] + ): this; + + off>( + eventName: EventName, + eventCallback: EventCallback + ): this; + + clear(): this; + + hasEvent(eventName: string): boolean; +} + +export type StoreAction< + State extends object, + Args extends unknown[] = unknown[], + Result = unknown +> = (state: State, ...args: Args) => Result; + +export type StoreActions = Record< + string, + StoreAction +>; + +export interface HYEventStoreOptions< + State extends object, + Actions extends StoreActions = Record +> { + state: State; + actions?: Actions & StoreActions; +} + +type StateKey = Extract; +type StateChange> = Partial< + Pick +>; +type ActionArguments = Action extends ( + state: State, + ...args: infer Args +) => unknown + ? Args + : never; +type ActionResult = Action extends (...args: any[]) => infer Result + ? Result + : never; + +export class HYEventStore< + State extends object, + Actions extends StoreActions = Record +> { + constructor(options: HYEventStoreOptions); + + state: State; + actions: Actions; + + onState>( + stateKey: Key, + stateCallback: (value: State[Key]) => void + ): void; + + offState>( + stateKey: Key, + stateCallback: (value: State[Key]) => void + ): void; + + onStates[]>( + stateKeys: Keys, + stateCallback: (change: StateChange) => void + ): void; + + offStates[]>( + stateKeys: Keys, + stateCallback: (change: StateChange) => void + ): void; + + setState>( + stateKey: Key, + stateValue: State[Key] + ): void; + + dispatch>( + actionName: ActionName, + ...args: ActionArguments + ): ActionResult; +} diff --git a/package-lock.json b/package-lock.json index 8ba9dac..d9eac33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,24 @@ "": { "name": "hy-event-store", "version": "1.4.0", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "typescript": "^5.7.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index 0ee2a68..adf64aa 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,20 @@ "version": "1.4.0", "description": "A zero-dependency event bus and global state store for Vue, React, Mini Programs, and vanilla JavaScript.", "main": "src/index.js", + "types": "index.d.ts", "files": [ "src", "CHANGELOG.md", - "README.zh-CN.md" + "README.zh-CN.md", + "index.d.ts" ], "scripts": { "test": "node --test Test/*.test.js", - "test:coverage": "node --test --experimental-test-coverage Test/*.test.js" + "test:coverage": "node --test --experimental-test-coverage Test/*.test.js", + "test:types": "tsc --noEmit --strict --skipLibCheck --module commonjs --moduleResolution node --target es2020 Test/types.test.ts" + }, + "devDependencies": { + "typescript": "^5.7.3" }, "author": "coderwhy", "license": "MIT",