@wcstack/state 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -75,6 +75,7 @@ CDNのスクリプトはカスタム要素の定義を登録するだけで、
75
75
  - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
76
76
  - **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
77
77
  - **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
78
+ - **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))
78
79
  - **依存ゼロ** — ランタイム依存なし
79
80
 
80
81
  ## インストール
@@ -1091,6 +1092,31 @@ bootstrapState({
1091
1092
  | `debug` | `false` | デバッグモード |
1092
1093
  | `enableMustache` | `true` | `{{ }}` 構文の有効化 |
1093
1094
 
1095
+ ## TypeScript サポート
1096
+
1097
+ `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。
1098
+
1099
+ ```typescript
1100
+ import { defineState } from '@wcstack/state';
1101
+
1102
+ export default defineState({
1103
+ count: 0,
1104
+ users: [] as { name: string; age: number }[],
1105
+
1106
+ increment() {
1107
+ this.count++; // ✅ number
1108
+ this["users.*.name"]; // ✅ string(ドットパス型解決)
1109
+ this.$getAll("users.*.age", []); // ✅ API メソッド
1110
+ },
1111
+
1112
+ get "users.*.ageCategory"() {
1113
+ return this["users.*.age"] < 25 ? "Young" : "Adult";
1114
+ }
1115
+ });
1116
+ ```
1117
+
1118
+ ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
1119
+
1094
1120
  ## API リファレンス
1095
1121
 
1096
1122
  ### `bootstrapState()`
package/README.md CHANGED
@@ -75,6 +75,7 @@ That's it. No build, no bootstrap code, no framework.
75
75
  - **Multiple state sources** — JSON, JS module, inline script, API, attribute
76
76
  - **SVG support** — full binding support inside `<svg>` elements
77
77
  - **Lifecycle hooks** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`, plus `$stateReadyCallback` for Web Components
78
+ - **TypeScript support** — `defineState()` for typed state definitions with dot-path autocompletion ([details](docs/define-state.md))
78
79
  - **Zero dependencies** — no runtime dependencies
79
80
 
80
81
  ## Installation
@@ -1091,6 +1092,31 @@ All options with defaults:
1091
1092
  | `debug` | `false` | Debug mode |
1092
1093
  | `enableMustache` | `true` | Enable `{{ }}` syntax |
1093
1094
 
1095
+ ## TypeScript Support
1096
+
1097
+ `defineState()` wraps your state object and provides type-safe `this` inside methods and getters — with zero runtime cost (identity function).
1098
+
1099
+ ```typescript
1100
+ import { defineState } from '@wcstack/state';
1101
+
1102
+ export default defineState({
1103
+ count: 0,
1104
+ users: [] as { name: string; age: number }[],
1105
+
1106
+ increment() {
1107
+ this.count++; // ✅ number
1108
+ this["users.*.name"]; // ✅ string (dot-path resolution)
1109
+ this.$getAll("users.*.age", []); // ✅ API method
1110
+ },
1111
+
1112
+ get "users.*.ageCategory"() {
1113
+ return this["users.*.age"] < 25 ? "Young" : "Adult";
1114
+ }
1115
+ });
1116
+ ```
1117
+
1118
+ Utility types `WcsPaths<T>` and `WcsPathValue<T, P>` are also exported for advanced use cases. See [docs/define-state.md](docs/define-state.md) for full documentation.
1119
+
1094
1120
  ## API Reference
1095
1121
 
1096
1122
  ### `bootstrapState()`
package/dist/index.d.ts CHANGED
@@ -16,5 +16,221 @@ interface IWritableConfig {
16
16
 
17
17
  declare function bootstrapState(config?: IWritableConfig): void;
18
18
 
19
- export { bootstrapState };
20
- export type { IWritableConfig, IWritableTagNames };
19
+ /**
20
+ * defineState.ts
21
+ *
22
+ * 状態オブジェクトに型付けを提供するためのユーティリティ。
23
+ * defineState() はアイデンティティ関数で、ThisType<> を付与することで
24
+ * メソッド・computed getter 内の this に型補完を提供する。
25
+ *
26
+ * テンプレートリテラル型によるドットパスの型解決:
27
+ * - WcsPaths<T> : T から生成される全ドットパスの union
28
+ * - WcsPathValue<T,P>: パス P に対応する値の型
29
+ * - WcsPathAccessor<T>: ブラケットアクセス用マップ型
30
+ */
31
+ /**
32
+ * `any` 型を検出する。
33
+ * `0 extends (1 & T)` は T が `any` の場合のみ true になる。
34
+ */
35
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
36
+ /**
37
+ * T がドットパス再帰の対象となる「プレーンなデータオブジェクト」かどうかを判定する。
38
+ * プリミティブ、組み込みオブジェクト (Date, Map 等)、関数、配列、any は除外。
39
+ */
40
+ type IsPlainObject<T> = IsAny<T> extends true ? false : T extends string | number | boolean | null | undefined | symbol | bigint | Function | Date | RegExp | Error | Map<any, any> | Set<any> | WeakMap<any, any> | WeakSet<any> | Promise<any> | readonly any[] ? false : T extends Record<string, any> ? true : false;
41
+ /**
42
+ * T のキーのうち、関数でないもの(データプロパティ・computed getter)を抽出する。
43
+ * メソッド(イベントハンドラ等)はドットパスの対象外。
44
+ * any 型のプロパティは除外せず保持する。
45
+ */
46
+ type DataKeys<T> = {
47
+ [K in keyof T & string]: IsAny<T[K]> extends true ? K : T[K] extends Function ? never : K;
48
+ }[keyof T & string];
49
+ /**
50
+ * 型 T から生成される全てのドットパスの union。
51
+ * 配列プロパティはワイルドカード `*` を使用: `items.*.name`
52
+ *
53
+ * 再帰の深さは最大4レベルに制限(コンパイル性能の確保)。
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * type S = {
58
+ * count: number;
59
+ * users: { name: string; age: number }[];
60
+ * cart: { items: { price: number }[] };
61
+ * };
62
+ * type P = WcsPaths<S>;
63
+ * // = "count" | "users" | "users.*" | "users.*.name" | "users.*.age"
64
+ * // | "cart" | "cart.items" | "cart.items.*" | "cart.items.*.price"
65
+ * ```
66
+ */
67
+ type WcsPaths<T, Depth extends readonly any[] = []> = Depth["length"] extends 4 ? never : {
68
+ [K in DataKeys<T>]: K | (T[K] extends readonly (infer E)[] ? IsPlainObject<E> extends true ? `${K}.*` | WcsSubPaths<E, `${K}.*.`, [...Depth, 0]> : `${K}.*` : IsPlainObject<T[K]> extends true ? WcsSubPaths<T[K], `${K}.`, [...Depth, 0]> : never);
69
+ }[DataKeys<T>];
70
+ /** @internal プレフィックス付きサブパスの生成ヘルパー */
71
+ type WcsSubPaths<T, Prefix extends string, Depth extends readonly any[]> = WcsPaths<T, Depth> extends infer P extends string ? `${Prefix}${P}` : never;
72
+ /**
73
+ * ドットパス P に対応する値の型を T から解決する。
74
+ *
75
+ * 解決順序:
76
+ * 1. T の直接キー(computed getter 含む)
77
+ * 2. `K.*` → 配列要素型
78
+ * 3. `K.rest` → オブジェクト/配列のネストを再帰的に辿る
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * type S = { cart: { items: { price: number; qty: number }[] } };
83
+ * type V1 = WcsPathValue<S, "cart.items.*.price">; // number
84
+ * type V2 = WcsPathValue<S, "cart.items.*">; // { price: number; qty: number }
85
+ * type V3 = WcsPathValue<S, "cart">; // { items: ... }
86
+ * ```
87
+ */
88
+ type WcsPathValue<T, P extends string> = P extends keyof T ? T[P] : P extends `${infer K}.*` ? K extends keyof T ? T[K] extends readonly (infer E)[] ? E : never : never : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends readonly (infer E)[] ? Rest extends `*.${infer SubRest}` ? WcsPathValue<E, SubRest> : Rest extends "*" ? E : never : T[K] extends Record<string, any> ? WcsPathValue<T[K], Rest> : never : never : never;
89
+ /**
90
+ * 全ドットパスに対する型付きブラケットアクセスを提供するマップ型。
91
+ *
92
+ * `this["users.*.name"]` のようなアクセスに対して、
93
+ * WcsPaths で生成されたパスに対応する値の型を返す。
94
+ */
95
+ type WcsPathAccessor<T> = {
96
+ [P in WcsPaths<T>]: WcsPathValue<T, P>;
97
+ };
98
+ /**
99
+ * `<wcs-state>` の Proxy 経由で提供されるAPIメソッド。
100
+ * state定義オブジェクト内のメソッド・getter で `this.` 経由で利用可能。
101
+ */
102
+ interface WcsStateApi {
103
+ /**
104
+ * ワイルドカードを含むパスにマッチする全要素を配列で取得する。
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * get "cart.totalPrice"() {
109
+ * return this.$getAll("cart.items.*.price", []).reduce((sum, v) => sum + v, 0);
110
+ * }
111
+ * ```
112
+ */
113
+ $getAll<V = any>(path: string, defaultValue?: V[]): V[];
114
+ /**
115
+ * 指定パスの更新を手動でトリガーする。
116
+ * Proxy の set トラップを経由せずに内部状態を変更した場合に使用。
117
+ */
118
+ $postUpdate(path: string): void;
119
+ /**
120
+ * パスとインデックス配列を指定して、ワイルドカードを解決した値を取得・設定する。
121
+ *
122
+ * @param path - ワイルドカードを含むパス
123
+ * @param indexes - 各ワイルドカード階層のインデックス
124
+ * @param value - 設定する値(省略時は取得)
125
+ */
126
+ $resolve(path: string, indexes: number[], value?: any): any;
127
+ /**
128
+ * 指定パスへの依存関係を明示的に登録する。
129
+ * computed getter 内で動的にパスを組み立てる場合に使用。
130
+ */
131
+ $trackDependency(path: string): void;
132
+ /** `<wcs-state>` 要素への参照 */
133
+ readonly $stateElement: HTMLElement;
134
+ readonly $1: number;
135
+ readonly $2: number;
136
+ readonly $3: number;
137
+ readonly $4: number;
138
+ readonly $5: number;
139
+ readonly $6: number;
140
+ readonly $7: number;
141
+ readonly $8: number;
142
+ readonly $9: number;
143
+ }
144
+ /**
145
+ * state定義オブジェクト内の `this` の型。
146
+ *
147
+ * - `T` のプロパティに型付きでアクセス可能(直接キー)
148
+ * - `WcsPathAccessor<T>` によるネストされたドットパスの型付きアクセス
149
+ * - `WcsStateApi` のメソッド ($getAll, $postUpdate 等) にアクセス可能
150
+ * - 動的パス (`this[\`items.${i}.name\`]`) は型チェック対象外(キャストが必要)
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * defineState({
155
+ * count: 0,
156
+ * users: [] as { name: string; age: number }[],
157
+ * increment() {
158
+ * this.count++; // number
159
+ * this["users.*.name"]; // string (パス型解決)
160
+ * this.$getAll("path", []); // API
161
+ * }
162
+ * });
163
+ * ```
164
+ */
165
+ type WcsThis<T> = T & WcsStateApi & WcsPathAccessor<T>;
166
+ /**
167
+ * `<wcs-state>` 用の型付き状態オブジェクトを定義する。
168
+ *
169
+ * ランタイムではアイデンティティ関数(引数をそのまま返す)として動作し、
170
+ * コストはゼロ。TypeScript の `ThisType<>` を利用して、メソッド・getter 内の
171
+ * `this` に型補完を提供する。
172
+ *
173
+ * ### 基本的な使い方 (TypeScript)
174
+ * ```ts
175
+ * import { defineState } from '@wcstack/state';
176
+ *
177
+ * export default defineState({
178
+ * count: 0,
179
+ * users: [] as { name: string; age: number }[],
180
+ *
181
+ * increment() {
182
+ * this.count++; // ✅ number
183
+ * this["users.*.name"]; // ✅ string (ドットパス型解決)
184
+ * },
185
+ *
186
+ * get "users.*.ageCategory"() {
187
+ * return this["users.*.age"] < 25 ? "Young" : "Adult";
188
+ * }
189
+ * });
190
+ * ```
191
+ *
192
+ * ### JavaScript (JSDoc)
193
+ * ```js
194
+ * import { defineState } from '@wcstack/state';
195
+ *
196
+ * export default defineState({
197
+ * count: 0,
198
+ * increment() {
199
+ * this.count++; // ✅ JSDoc + tsconfig checkJs で型補完
200
+ * }
201
+ * });
202
+ * ```
203
+ *
204
+ * ### HTML インラインスクリプト
205
+ * ```html
206
+ * <wcs-state>
207
+ * <script type="module">
208
+ * import { defineState } from '@wcstack/state';
209
+ * export default defineState({
210
+ * count: 0,
211
+ * increment() { this.count++; }
212
+ * });
213
+ * </script>
214
+ * </wcs-state>
215
+ * ```
216
+ *
217
+ * ### ライフサイクルコールバック
218
+ * ```ts
219
+ * export default defineState({
220
+ * data: null,
221
+ * async $connectedCallback() {
222
+ * this.data = await fetch('/api/data').then(r => r.json());
223
+ * },
224
+ * $disconnectedCallback() {
225
+ * // cleanup
226
+ * },
227
+ * $updatedCallback() {
228
+ * // called after DOM update
229
+ * }
230
+ * });
231
+ * ```
232
+ */
233
+ declare function defineState<T extends Record<string, any>>(definition: T & ThisType<WcsThis<T>>): T;
234
+
235
+ export { bootstrapState, defineState };
236
+ export type { IWritableConfig, IWritableTagNames, WcsPathValue, WcsPaths, WcsStateApi, WcsThis };
package/dist/index.esm.js CHANGED
@@ -3014,19 +3014,50 @@ const applyChangeByBindingType = {
3014
3014
  "radio": applyChangeToRadio,
3015
3015
  "checkbox": applyChangeToCheckbox,
3016
3016
  };
3017
+ const fnByBinding = new WeakMap();
3018
+ const deferredSelectBindingByBinding = new WeakMap();
3017
3019
  function _applyChange(binding, context) {
3018
3020
  const value = getValue(context.state, binding);
3019
3021
  const filteredValue = getFilteredValue(value, binding.outFilters);
3020
- let fn = applyChangeByBindingType[binding.bindingType];
3022
+ if (deferredSelectBindingByBinding.get(binding) === true) {
3023
+ context.deferredSelectBindings.push({ binding, value: filteredValue });
3024
+ return;
3025
+ }
3026
+ let fn = fnByBinding.get(binding);
3027
+ if (typeof fn !== 'undefined') {
3028
+ fn(binding, context, filteredValue);
3029
+ return;
3030
+ }
3031
+ if (fnByBinding.has(binding)) {
3032
+ if (isWebComponentComplete(binding.replaceNode, context.stateElement)) {
3033
+ fn = applyChangeToWebComponent;
3034
+ fnByBinding.set(binding, fn); // 確定したのでキャッシュ
3035
+ }
3036
+ else {
3037
+ fn = applyChangeToProperty;
3038
+ }
3039
+ fn(binding, context, filteredValue);
3040
+ return;
3041
+ }
3042
+ fn = applyChangeByBindingType[binding.bindingType];
3021
3043
  if (typeof fn === 'undefined') {
3022
3044
  const firstSegment = binding.propSegments[0];
3023
3045
  fn = applyChangeByFirstSegment[firstSegment];
3046
+ fnByBinding.set(binding, fn);
3024
3047
  if (typeof fn === 'undefined') {
3025
- if (isWebComponentComplete(binding.replaceNode, context.stateElement)) {
3026
- fn = applyChangeToWebComponent;
3048
+ const customTag = getCustomElement(binding.replaceNode);
3049
+ if (customTag) {
3050
+ if (isWebComponentComplete(binding.replaceNode, context.stateElement)) {
3051
+ fn = applyChangeToWebComponent;
3052
+ fnByBinding.set(binding, fn); // 確定したのでキャッシュ
3053
+ }
3054
+ else {
3055
+ fn = applyChangeToProperty;
3056
+ }
3027
3057
  }
3028
3058
  else {
3029
3059
  fn = applyChangeToProperty;
3060
+ fnByBinding.set(binding, fn);
3030
3061
  }
3031
3062
  }
3032
3063
  }
@@ -3036,6 +3067,7 @@ function _applyChange(binding, context) {
3036
3067
  const propName = binding.propSegments[0];
3037
3068
  if (propName === 'value' || propName === 'selectedIndex') {
3038
3069
  context.deferredSelectBindings.push({ binding, value: filteredValue });
3070
+ deferredSelectBindingByBinding.set(binding, true);
3039
3071
  return;
3040
3072
  }
3041
3073
  }
@@ -5813,5 +5845,91 @@ function bootstrapState(config) {
5813
5845
  registerComponents();
5814
5846
  }
5815
5847
 
5816
- export { bootstrapState };
5848
+ /**
5849
+ * defineState.ts
5850
+ *
5851
+ * 状態オブジェクトに型付けを提供するためのユーティリティ。
5852
+ * defineState() はアイデンティティ関数で、ThisType<> を付与することで
5853
+ * メソッド・computed getter 内の this に型補完を提供する。
5854
+ *
5855
+ * テンプレートリテラル型によるドットパスの型解決:
5856
+ * - WcsPaths<T> : T から生成される全ドットパスの union
5857
+ * - WcsPathValue<T,P>: パス P に対応する値の型
5858
+ * - WcsPathAccessor<T>: ブラケットアクセス用マップ型
5859
+ */
5860
+ // ============================================================
5861
+ // defineState — 型付き状態定義関数
5862
+ // ============================================================
5863
+ /**
5864
+ * `<wcs-state>` 用の型付き状態オブジェクトを定義する。
5865
+ *
5866
+ * ランタイムではアイデンティティ関数(引数をそのまま返す)として動作し、
5867
+ * コストはゼロ。TypeScript の `ThisType<>` を利用して、メソッド・getter 内の
5868
+ * `this` に型補完を提供する。
5869
+ *
5870
+ * ### 基本的な使い方 (TypeScript)
5871
+ * ```ts
5872
+ * import { defineState } from '@wcstack/state';
5873
+ *
5874
+ * export default defineState({
5875
+ * count: 0,
5876
+ * users: [] as { name: string; age: number }[],
5877
+ *
5878
+ * increment() {
5879
+ * this.count++; // ✅ number
5880
+ * this["users.*.name"]; // ✅ string (ドットパス型解決)
5881
+ * },
5882
+ *
5883
+ * get "users.*.ageCategory"() {
5884
+ * return this["users.*.age"] < 25 ? "Young" : "Adult";
5885
+ * }
5886
+ * });
5887
+ * ```
5888
+ *
5889
+ * ### JavaScript (JSDoc)
5890
+ * ```js
5891
+ * import { defineState } from '@wcstack/state';
5892
+ *
5893
+ * export default defineState({
5894
+ * count: 0,
5895
+ * increment() {
5896
+ * this.count++; // ✅ JSDoc + tsconfig checkJs で型補完
5897
+ * }
5898
+ * });
5899
+ * ```
5900
+ *
5901
+ * ### HTML インラインスクリプト
5902
+ * ```html
5903
+ * <wcs-state>
5904
+ * <script type="module">
5905
+ * import { defineState } from '@wcstack/state';
5906
+ * export default defineState({
5907
+ * count: 0,
5908
+ * increment() { this.count++; }
5909
+ * });
5910
+ * </script>
5911
+ * </wcs-state>
5912
+ * ```
5913
+ *
5914
+ * ### ライフサイクルコールバック
5915
+ * ```ts
5916
+ * export default defineState({
5917
+ * data: null,
5918
+ * async $connectedCallback() {
5919
+ * this.data = await fetch('/api/data').then(r => r.json());
5920
+ * },
5921
+ * $disconnectedCallback() {
5922
+ * // cleanup
5923
+ * },
5924
+ * $updatedCallback() {
5925
+ * // called after DOM update
5926
+ * }
5927
+ * });
5928
+ * ```
5929
+ */
5930
+ function defineState(definition) {
5931
+ return definition;
5932
+ }
5933
+
5934
+ export { bootstrapState, defineState };
5817
5935
  //# sourceMappingURL=index.esm.js.map