chem-rx 0.0.1 → 0.0.2
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.md +268 -30
- package/package.json +1 -1
- package/src/Atom.ts +122 -20
- package/src/Signal.ts +4 -14
- package/src/index.ts +1 -0
- package/src/useAtom.ts +3 -3
- package/src/useSelectAtom.ts +24 -0
- package/tests/atom.test.ts +242 -19
- package/tests/sample.ts +123 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chem-rx
|
|
2
2
|
|
|
3
|
-
`chem-rx` wraps`
|
|
3
|
+
`chem-rx` wraps `rxjs` to provide a state management solution focused on
|
|
4
4
|
simplicity. Useable with or without React!
|
|
5
5
|
|
|
6
6
|
## Atom
|
|
@@ -14,10 +14,10 @@ Atoms are state containers that take any value - object, array, or primitive.
|
|
|
14
14
|
```
|
|
15
15
|
import { Atom } from 'chem-rx'
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
17
|
+
const number$: BaseAtom = Atom(0)
|
|
18
|
+
const string$: BaseAtom = Atom('hello')
|
|
19
|
+
const array$: ArrayAtom = Atom(['hello', 'world'])
|
|
20
|
+
const object$: ObjectAtom = Atom({ 'hello': 'world', 'world': 'hello' })
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### Getting & setting values
|
|
@@ -26,55 +26,293 @@ const objectAtom: ObjectAtom = Atom({ 'hello': 'world', 'world': 'hello' })
|
|
|
26
26
|
`ObjectAtom` depending on the input.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
numberAtom.set(2)
|
|
33
|
-
numberAtom.value()
|
|
29
|
+
// Base Atom
|
|
30
|
+
number$.set(2)
|
|
31
|
+
number$.value()
|
|
34
32
|
// 2
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
arrayAtom.push('!')
|
|
40
|
-
arrayAtom.value()
|
|
34
|
+
// ArrayAtom
|
|
35
|
+
array$.push('!')
|
|
36
|
+
array$.value()
|
|
41
37
|
// ['hello', 'world', '!']
|
|
42
|
-
|
|
38
|
+
array$.get(1)
|
|
43
39
|
// 'world'
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
objectAtom.set('world', 'hi')
|
|
49
|
-
objectAtom.value()
|
|
41
|
+
// ObjectAtom
|
|
42
|
+
object$.set('world', 'hi')
|
|
43
|
+
object$.value()
|
|
50
44
|
// {'hello': 'world', 'world': 'hi'}
|
|
51
45
|
|
|
52
|
-
|
|
46
|
+
object$.set('sup', 'earth')
|
|
53
47
|
// {'hello': 'world', 'world': 'hi', 'sup': 'earth'}
|
|
54
48
|
|
|
55
|
-
|
|
49
|
+
object$.get('world')
|
|
56
50
|
// 'hi'
|
|
57
51
|
```
|
|
58
52
|
|
|
59
|
-
###
|
|
53
|
+
### Selecting Atoms
|
|
54
|
+
|
|
55
|
+
You can select Object and Array atoms to return new Atoms that wrap the values
|
|
56
|
+
at that key. This can be useful for working with different parts of nested Array
|
|
57
|
+
and Object atoms.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
const nestedData = Atom({
|
|
61
|
+
stacy: {
|
|
62
|
+
nickname: "stace",
|
|
63
|
+
education: {
|
|
64
|
+
school: "Penn",
|
|
65
|
+
graduation: 2014,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const stacy = nestedData.select("stacy");
|
|
71
|
+
console.log(stacy.get('nickname'))
|
|
72
|
+
// 'stace'
|
|
73
|
+
|
|
74
|
+
const stacySchool = nestedData.select("stacy").select("education");
|
|
75
|
+
console.log(stacySchool.get('school'))
|
|
76
|
+
// 'Penn'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Derived Atoms (read-only)
|
|
80
|
+
|
|
81
|
+
You can derive new Atoms from any existing atoms. Any time the original atoms
|
|
82
|
+
change, your derived atoms will automatically update with new values!
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
const atom$ = Atom(3);
|
|
86
|
+
|
|
87
|
+
// square it
|
|
88
|
+
const squared$ = atom$.derive((x) => x * x);
|
|
89
|
+
|
|
90
|
+
// "9"
|
|
91
|
+
console.log(squared$.value())
|
|
92
|
+
|
|
93
|
+
// Update the original value
|
|
94
|
+
atom$.set(4)
|
|
95
|
+
|
|
96
|
+
// "16"
|
|
97
|
+
console.log(squared$.value())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Every derived atom is **read-only**. This prevents you from overriding the
|
|
101
|
+
derived output value, since it is automatically derived from another input!
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
// ERR: Property 'set' does not exist on type ReadOnlyAtom
|
|
105
|
+
squared$.set(2)
|
|
106
|
+
```
|
|
60
107
|
|
|
61
|
-
|
|
108
|
+
You can optionally enforce `readOnly` on an atom at creation time if needed
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
const atom$ = Atom(3, true);
|
|
112
|
+
|
|
113
|
+
// ERR: Property 'set' does not exist on type ReadOnlyAtom
|
|
114
|
+
atom$.set(2)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Combining Atoms
|
|
118
|
+
|
|
119
|
+
Multiple atoms can also be **combined** to create brand new atoms!
|
|
120
|
+
|
|
121
|
+
Here's an example of joining a set of normalized data models
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
const pets$ = Atom<{ [name: string]: { type: "dog" | "cat"; age: number } }>({
|
|
125
|
+
spot: {name: 'spot', type: "dog", age: 5 },
|
|
126
|
+
tabby: {name: 'tabby', type: "cat", age: 12 },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const people$ = Atom<{ [name: string]: { pets: string[] } }>({
|
|
130
|
+
mary: { pets: ["spot"] },
|
|
131
|
+
cam: { pets: ["tabby"] },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const mary$ = Atom.combine(pets$, people$.select("mary")).derive(
|
|
135
|
+
([pets, mary]) => {
|
|
136
|
+
return {
|
|
137
|
+
...mary,
|
|
138
|
+
pets: mary.pets.map((petName) => pets[petName]),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
console.log(mary$.select('pets').value())
|
|
144
|
+
/*
|
|
145
|
+
* [{
|
|
146
|
+
* name: "spot",
|
|
147
|
+
* type: "dog",
|
|
148
|
+
* age: 5,
|
|
149
|
+
* }]
|
|
150
|
+
*/
|
|
151
|
+
```
|
|
62
152
|
|
|
63
153
|
### Subscribing to updates
|
|
64
154
|
|
|
155
|
+
Atoms emit values each time they're updated. You can subscribe callbacks to them
|
|
156
|
+
to act on updates
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
const atom$ = Atom(3);
|
|
160
|
+
|
|
161
|
+
const subscription = atom$.subscribe(val => {
|
|
162
|
+
console.log("Received value: ", val)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
atom$.set(4)
|
|
166
|
+
// "Received value: 4"
|
|
167
|
+
|
|
168
|
+
// Unsubscribe to clean up
|
|
169
|
+
subscription.unsubscribe();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Signals
|
|
173
|
+
|
|
174
|
+
Sometimes, all you want is something to ping you when there's an update. Signals
|
|
175
|
+
are stateless transceivers for signaling updates.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
const signal$ = new Signal();
|
|
179
|
+
|
|
180
|
+
const subscription = signal$.subscribe(() => {
|
|
181
|
+
console.log("PONG")
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
signal$.ping()
|
|
185
|
+
// "PONG"
|
|
186
|
+
|
|
187
|
+
// Unsubscribe to clean up
|
|
188
|
+
subscription.unsubscribe();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Signals can also send values if needed.
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
const signal$ = new Signal();
|
|
195
|
+
|
|
196
|
+
const subscription = signal$.subscribe((value) => {
|
|
197
|
+
console.log("PONGED: ", value)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
signal$.ping("hello")
|
|
201
|
+
// "PONGED: hello"
|
|
202
|
+
|
|
203
|
+
// Unsubscribe to clean up
|
|
204
|
+
subscription.unsubscribe();
|
|
205
|
+
```
|
|
206
|
+
|
|
65
207
|
## Use with React
|
|
66
208
|
|
|
67
209
|
### useAtom
|
|
68
210
|
|
|
211
|
+
`useAtom` automatically updates with new values in your react components.
|
|
212
|
+
|
|
213
|
+
If you want to update your atoms, you can simply call the same `set`
|
|
214
|
+
(Base/ObjectAtom) or `push` (ArrayAtom) methods you would typically use outside
|
|
215
|
+
of react.
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
import { Atom, useAtom } from 'chem-rx'
|
|
219
|
+
|
|
220
|
+
const count$ = Atom(0)
|
|
221
|
+
|
|
222
|
+
function Counter() {
|
|
223
|
+
const count = useAtom(count$)
|
|
224
|
+
return (
|
|
225
|
+
<h1>
|
|
226
|
+
{count}
|
|
227
|
+
<button onClick={() => count$.set(count$.value() + 1)}>one up</button> ...
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Remember that you can mix and match for any of your needs
|
|
231
|
+
|
|
232
|
+
### useSelectAtom
|
|
233
|
+
|
|
234
|
+
With `useSelect` can select a specific key from an atom, and still have it live
|
|
235
|
+
update in your react component.
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
import { Atom, useAtom } from 'chem-rx'
|
|
239
|
+
|
|
240
|
+
const count$ = Atom({ inner: 0 })
|
|
241
|
+
|
|
242
|
+
function Counter() {
|
|
243
|
+
const count = useSelectAtom(count$, 'inner')
|
|
244
|
+
return (
|
|
245
|
+
<h1>
|
|
246
|
+
{count}
|
|
247
|
+
<button onClick={() => count$.set('inner', count + 2)}>one up</button> ...
|
|
248
|
+
```
|
|
249
|
+
|
|
69
250
|
### hydrateAtoms
|
|
70
251
|
|
|
71
|
-
|
|
252
|
+
With SSR, your atoms will likely need to be properly hydrated to prevent
|
|
253
|
+
server/client mismatches. You can use `hydrateAtoms` as a simple solution for
|
|
254
|
+
seeding your client-side Atoms with the correct data.
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
import { Atom, useAtom, hydrateAtoms } from 'chem-rx'
|
|
258
|
+
|
|
259
|
+
const count$ = Atom(0)
|
|
260
|
+
const CounterPage = ({ countFromServer }) => {
|
|
261
|
+
hydrateAtoms([[count$, countFromServer]])
|
|
262
|
+
const count = useAtom(count$)
|
|
263
|
+
// count would be the value of `countFromServer`, not 0.
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Suggested Usage
|
|
268
|
+
|
|
269
|
+
There are several suggested "patterns" when using Atoms:
|
|
270
|
+
|
|
271
|
+
1. Suffix all atoms with `$` (for readability).
|
|
272
|
+
2. Keep all data management **outside** of your views (e.g, React)
|
|
273
|
+
3. Avoid using `set` and `push` directly from your client components. Instead,
|
|
274
|
+
create helper functions (actions)
|
|
275
|
+
4. Name your helper actions as `<atomName>$<actionName>`, to easily see what
|
|
276
|
+
atoms are involved.
|
|
277
|
+
5. Name your derived atoms as `<baseAtom>_<derivedValue>$` to easily see which
|
|
278
|
+
atoms it derives from.
|
|
279
|
+
|
|
280
|
+
## Common Issues
|
|
281
|
+
|
|
282
|
+
Here are some common issues you might run into when starting out.
|
|
283
|
+
|
|
284
|
+
1. Keep your atoms in separate files to prevent circular dependencies.
|
|
285
|
+
1. I typically create a new file for every action, so I can easily see the
|
|
286
|
+
API surface at a glance
|
|
287
|
+
|
|
288
|
+
## Advanced Usage with `rxjs`
|
|
289
|
+
|
|
290
|
+
Behind the scenes, `chem-rx` uses
|
|
291
|
+
[rxjs Observables](https://rxjs.dev/guide/operators) to enable reactivity.
|
|
292
|
+
`Atom` abstracts away the majority of Rx intentionally, to extract the most
|
|
293
|
+
common patterns used when managing front-end data.
|
|
294
|
+
|
|
295
|
+
If you're coming in with prior experience and are seeking more complex operators
|
|
296
|
+
enabled by Rx, you're in luck, because every Atom is simply a wrapper around a
|
|
297
|
+
`BehaviorSubject`!
|
|
298
|
+
|
|
299
|
+
You can use any rxjs operations you want with `Atom.pipe`, which wraps
|
|
300
|
+
`Observable.pipe` to return an Atom.
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
import { map } from "rxjs";
|
|
304
|
+
|
|
305
|
+
const atom$ = Atom(3);
|
|
306
|
+
|
|
307
|
+
// Replace `map` with any operators from rxjs
|
|
308
|
+
const squared$: Atom<number> = atom.pipe(map((x) => x * x));
|
|
309
|
+
|
|
310
|
+
// "9"
|
|
311
|
+
console.log(squared$.value());
|
|
312
|
+
```
|
|
72
313
|
|
|
73
314
|
## Why...?
|
|
74
315
|
|
|
75
316
|
This library spawned out of a love for the flexibility and expressiveness of
|
|
76
|
-
[
|
|
317
|
+
[rxjs](https://github.com/ReactiveX/rxjs) Observables, and the simplicity of
|
|
77
318
|
atomic libraries like [jotai](https://github.com/pmndrs/jotai).
|
|
78
|
-
|
|
79
|
-
Its primary focus is on ease of use and code cleanliness, and is my go-to
|
|
80
|
-
library for all client-side state management
|
package/package.json
CHANGED
package/src/Atom.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BehaviorSubject,
|
|
3
3
|
combineLatest,
|
|
4
|
+
distinctUntilKeyChanged,
|
|
4
5
|
isObservable,
|
|
5
6
|
map,
|
|
6
7
|
Observable,
|
|
7
8
|
OperatorFunction,
|
|
8
9
|
Subscription,
|
|
9
10
|
} from "rxjs";
|
|
10
|
-
import { OverloadedParameters
|
|
11
|
+
import { OverloadedParameters } from "./types";
|
|
11
12
|
|
|
12
13
|
export type AtomTuple<T> = {
|
|
13
14
|
[K in keyof T]: ReadOnlyAtom<T[K]>;
|
|
@@ -112,7 +113,6 @@ export class ReadOnlyAtom<T> {
|
|
|
112
113
|
op9: OperatorFunction<H, I>,
|
|
113
114
|
...operations: OperatorFunction<any, any>[]
|
|
114
115
|
): ReadOnlyAtom<unknown>;
|
|
115
|
-
|
|
116
116
|
pipe(
|
|
117
117
|
...operations: OverloadedParameters<Observable<T>["pipe"]>
|
|
118
118
|
): ReadOnlyAtom<any> {
|
|
@@ -123,8 +123,8 @@ export class ReadOnlyAtom<T> {
|
|
|
123
123
|
return newAtom;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
return this.pipe(map(
|
|
126
|
+
derive<A>(deriveFn: (value: T, index: number) => A): ReadOnlyAtom<A> {
|
|
127
|
+
return this.pipe(map(deriveFn));
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
subscribe(...params: Parameters<BehaviorSubject<T>["subscribe"]>) {
|
|
@@ -139,6 +139,45 @@ export class ReadOnlyAtom<T> {
|
|
|
139
139
|
dispose() {
|
|
140
140
|
this._fromObservableSubscription?.unsubscribe();
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
get(
|
|
144
|
+
key: T extends (infer W)[]
|
|
145
|
+
? number
|
|
146
|
+
: T extends { [key in keyof T]: infer W }
|
|
147
|
+
? keyof T
|
|
148
|
+
: undefined
|
|
149
|
+
): T extends (infer W)[]
|
|
150
|
+
? T[number]
|
|
151
|
+
: T extends { [key in keyof T]: infer W }
|
|
152
|
+
? T[keyof T]
|
|
153
|
+
: undefined {
|
|
154
|
+
const val = this.value() as T;
|
|
155
|
+
// @ts-ignore Can't figure out this type so i'm REALLY cheating
|
|
156
|
+
return val[key] as T extends (infer W)[]
|
|
157
|
+
? T[number]
|
|
158
|
+
: T extends { [key in keyof T]: infer W }
|
|
159
|
+
? T[keyof T]
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
select<K extends keyof T>(
|
|
164
|
+
key: K
|
|
165
|
+
): T[K] extends (infer W)[]
|
|
166
|
+
? ArrayAtom<W>
|
|
167
|
+
: T[K] extends { [key: string | symbol]: infer W }
|
|
168
|
+
? ObjectAtom<T[K]>
|
|
169
|
+
: BaseAtom<T[K]> {
|
|
170
|
+
const newObs = this._behavior$.pipe(
|
|
171
|
+
distinctUntilKeyChanged(key),
|
|
172
|
+
map((k) => k?.[key])
|
|
173
|
+
);
|
|
174
|
+
// Can't get typescript to recognize the types here so I'm cheating
|
|
175
|
+
return Atom(newObs) as unknown as T[K] extends (infer W)[]
|
|
176
|
+
? ArrayAtom<W>
|
|
177
|
+
: T[K] extends { [key: string | symbol]: infer W }
|
|
178
|
+
? ObjectAtom<T[K]>
|
|
179
|
+
: BaseAtom<T[K]>;
|
|
180
|
+
}
|
|
142
181
|
}
|
|
143
182
|
|
|
144
183
|
export class BaseAtom<T> extends ReadOnlyAtom<T> {
|
|
@@ -148,17 +187,51 @@ export class BaseAtom<T> extends ReadOnlyAtom<T> {
|
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
export class ArrayAtom<T> extends ReadOnlyAtom<T[]> {
|
|
190
|
+
constructor(initialValue: T[]) {
|
|
191
|
+
super(initialValue);
|
|
192
|
+
}
|
|
193
|
+
|
|
151
194
|
push(nextVal: T) {
|
|
152
195
|
this._behavior$.next([...this._behavior$.getValue(), nextVal]);
|
|
153
196
|
}
|
|
154
|
-
get(idx: number) {
|
|
155
|
-
return this.value()[idx];
|
|
156
|
-
}
|
|
157
197
|
}
|
|
198
|
+
//
|
|
199
|
+
// export class ReadOnlyObjectAtom<
|
|
200
|
+
// T extends {
|
|
201
|
+
// [key in K]: V;
|
|
202
|
+
// },
|
|
203
|
+
// K extends string | number | symbol = keyof T,
|
|
204
|
+
// V = T[K]
|
|
205
|
+
// > extends ReadOnlyAtom<T> {
|
|
206
|
+
// get(nextKey: K) {
|
|
207
|
+
// return this.value()[nextKey];
|
|
208
|
+
// }
|
|
209
|
+
//
|
|
210
|
+
// select<K extends keyof T>(
|
|
211
|
+
// key: K
|
|
212
|
+
// ): T[K] extends (infer W)[]
|
|
213
|
+
// ? ArrayAtom<W>
|
|
214
|
+
// : T[K] extends { [key in infer L]?: infer W }
|
|
215
|
+
// ? ObjectAtom<T[K]>
|
|
216
|
+
// : BaseAtom<T> {
|
|
217
|
+
// const newObs = this._behavior$.pipe(
|
|
218
|
+
// distinctUntilKeyChanged(key),
|
|
219
|
+
// map((k) => k?.[key])
|
|
220
|
+
// );
|
|
221
|
+
// // Can't get typescript to recognize the types here so I'm cheating
|
|
222
|
+
// return Atom(newObs) as unknown as T[K] extends (infer W)[]
|
|
223
|
+
// ? ArrayAtom<W>
|
|
224
|
+
// : T[K] extends { [key in infer L]?: infer W }
|
|
225
|
+
// ? ObjectAtom<T[K]>
|
|
226
|
+
// : BaseAtom<T>;
|
|
227
|
+
// }
|
|
228
|
+
// }
|
|
158
229
|
|
|
159
230
|
export class ObjectAtom<
|
|
160
|
-
T extends
|
|
161
|
-
|
|
231
|
+
T extends {
|
|
232
|
+
[key in K]: V;
|
|
233
|
+
},
|
|
234
|
+
K extends string | number | symbol = keyof T,
|
|
162
235
|
V = T[K]
|
|
163
236
|
> extends ReadOnlyAtom<T> {
|
|
164
237
|
set(nextKey: K, nextValue: V) {
|
|
@@ -167,21 +240,50 @@ export class ObjectAtom<
|
|
|
167
240
|
[nextKey]: nextValue,
|
|
168
241
|
});
|
|
169
242
|
}
|
|
170
|
-
get(nextKey: K) {
|
|
171
|
-
return this.value()[nextKey];
|
|
172
|
-
}
|
|
173
243
|
}
|
|
174
244
|
|
|
175
|
-
|
|
176
|
-
export
|
|
177
|
-
|
|
178
|
-
)
|
|
179
|
-
export function Atom<T>(_value: Observable<T> | T): BaseAtom<T>;
|
|
245
|
+
// catch-all for developers
|
|
246
|
+
// export type AnyAtom<T> = BaseAtom<T> | ArrayAtom<T> | ObjectAtom<T>;
|
|
247
|
+
|
|
248
|
+
// observable type (primitive)
|
|
180
249
|
export function Atom<T>(
|
|
181
|
-
|
|
182
|
-
): BaseAtom<T
|
|
250
|
+
value: T extends { [key: string]: infer V } | any[] ? never : Observable<T>
|
|
251
|
+
): BaseAtom<T>;
|
|
252
|
+
|
|
253
|
+
// observable<array> type
|
|
254
|
+
export function Atom<T extends any[]>(
|
|
255
|
+
value: Observable<T>
|
|
256
|
+
): ArrayAtom<T[number]>;
|
|
257
|
+
|
|
258
|
+
// observable<object> type
|
|
259
|
+
export function Atom<T>(
|
|
260
|
+
value: T extends {
|
|
261
|
+
[key in keyof T]: infer V;
|
|
262
|
+
}
|
|
263
|
+
? Observable<T>
|
|
264
|
+
: never
|
|
265
|
+
): ObjectAtom<T>;
|
|
266
|
+
|
|
267
|
+
// array type
|
|
268
|
+
export function Atom<T extends any[]>(value: T): ArrayAtom<T[number]>;
|
|
269
|
+
|
|
270
|
+
// object type
|
|
271
|
+
export function Atom<T extends { [key: string]: T[keyof T] }>(
|
|
272
|
+
value: T
|
|
273
|
+
): ObjectAtom<T>;
|
|
274
|
+
|
|
275
|
+
// primitive type
|
|
276
|
+
export function Atom<T>(value: T): BaseAtom<T>;
|
|
277
|
+
|
|
278
|
+
// readonly type
|
|
279
|
+
export function Atom<T>(value: T, readOnly?: boolean): ReadOnlyAtom<T>;
|
|
280
|
+
|
|
281
|
+
// function definition
|
|
282
|
+
export function Atom<T>(_value: T, readOnly: boolean = false) {
|
|
183
283
|
let atom;
|
|
184
|
-
if (
|
|
284
|
+
if (readOnly) {
|
|
285
|
+
atom = new ReadOnlyAtom(_value);
|
|
286
|
+
} else if (Array.isArray(_value)) {
|
|
185
287
|
atom = new ArrayAtom<T>(_value); // For arrays
|
|
186
288
|
} else if (typeof _value === "object" && _value !== null) {
|
|
187
289
|
atom = new ObjectAtom<T>(_value); // For objects
|
package/src/Signal.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Subject,
|
|
3
|
-
isObservable,
|
|
4
|
-
Observable,
|
|
5
|
-
OperatorFunction,
|
|
6
|
-
Subscription,
|
|
7
|
-
} from "rxjs";
|
|
8
|
-
import { OverloadedParameters, OverloadedReturnType } from "./types";
|
|
1
|
+
import { Subject } from "rxjs";
|
|
9
2
|
|
|
10
|
-
export class
|
|
3
|
+
export class BaseSignal<T = any> {
|
|
11
4
|
_subject$: Subject<T>;
|
|
12
5
|
|
|
13
6
|
constructor() {
|
|
@@ -22,9 +15,6 @@ export class Signal<T = void> {
|
|
|
22
15
|
subscribe(...params: Parameters<Subject<T>["subscribe"]>) {
|
|
23
16
|
return this._subject$.subscribe(...params);
|
|
24
17
|
}
|
|
25
|
-
|
|
26
|
-
// not needed?
|
|
27
|
-
dispose() {
|
|
28
|
-
this._subject$.unsubscribe();
|
|
29
|
-
}
|
|
30
18
|
}
|
|
19
|
+
|
|
20
|
+
export function Signal<T>() {}
|
package/src/index.ts
CHANGED
package/src/useAtom.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ReadOnlyAtom } from "./Atom";
|
|
3
3
|
|
|
4
|
-
export function useAtom<T>(atom:
|
|
5
|
-
const [value, setValue] = useState<T>(atom.
|
|
4
|
+
export function useAtom<T>(atom: ReadOnlyAtom<T>): T {
|
|
5
|
+
const [value, setValue] = useState<T>(atom.value());
|
|
6
6
|
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
const subscription = atom.subscribe((val) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { ObjectAtom } from "./Atom";
|
|
3
|
+
|
|
4
|
+
export function useSelectAtom<
|
|
5
|
+
T extends {
|
|
6
|
+
[key in K]: V;
|
|
7
|
+
},
|
|
8
|
+
K extends string | number | symbol = keyof T,
|
|
9
|
+
V = T[K]
|
|
10
|
+
>(atom: ObjectAtom<T>, key: K): T[K] {
|
|
11
|
+
const [value, setValue] = useState<T[K]>(atom.get(key));
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const subscription = atom.select(key).subscribe((val) => {
|
|
15
|
+
setValue(val as T[K]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
subscription.unsubscribe();
|
|
20
|
+
};
|
|
21
|
+
}, [atom]);
|
|
22
|
+
|
|
23
|
+
return value;
|
|
24
|
+
}
|
package/tests/atom.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
ObjectAtom,
|
|
6
6
|
ReadOnlyAtom,
|
|
7
7
|
} from "../src/Atom";
|
|
8
|
-
import {
|
|
8
|
+
import { BehaviorSubject, map } from "rxjs";
|
|
9
9
|
|
|
10
10
|
test("Base Atom values test", () => {
|
|
11
11
|
const atom = Atom("aweofij");
|
|
@@ -17,6 +17,15 @@ test("Base Atom values test", () => {
|
|
|
17
17
|
expect(atom.value()).toBe("apro");
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
test("Test readonly", () => {
|
|
21
|
+
const atom = Atom({ a: 10 }, true);
|
|
22
|
+
expect(atom instanceof ReadOnlyAtom).toBe(true);
|
|
23
|
+
expect(atom instanceof BaseAtom).toBe(false);
|
|
24
|
+
expect(atom.get("a")).toBe(10);
|
|
25
|
+
|
|
26
|
+
expect(atom).not.toHaveProperty("set");
|
|
27
|
+
});
|
|
28
|
+
|
|
20
29
|
test("Object Atom values test", () => {
|
|
21
30
|
const atom = Atom<{ [key: string]: string }>({
|
|
22
31
|
firstKey: "firstValue",
|
|
@@ -53,8 +62,27 @@ test("Object Atom get function test", () => {
|
|
|
53
62
|
expect(atom.get("thirdKey")).toBe("thirdValue");
|
|
54
63
|
});
|
|
55
64
|
|
|
65
|
+
test("Object Enum Atom test", () => {
|
|
66
|
+
enum testEnum {
|
|
67
|
+
first,
|
|
68
|
+
second,
|
|
69
|
+
}
|
|
70
|
+
const atom = Atom({
|
|
71
|
+
[testEnum.first]: "firstValue",
|
|
72
|
+
[testEnum.second]: "secondValue",
|
|
73
|
+
});
|
|
74
|
+
expect(atom instanceof ObjectAtom).toBe(true);
|
|
75
|
+
expect(atom.get(testEnum.first)).toBe("firstValue");
|
|
76
|
+
|
|
77
|
+
expect(atom.get(testEnum.second)).toBe("secondValue");
|
|
78
|
+
atom.set(testEnum.second, "newSecondValue");
|
|
79
|
+
expect(atom.get(testEnum.second)).toBe("newSecondValue");
|
|
80
|
+
});
|
|
81
|
+
|
|
56
82
|
test("Array Atom values test", () => {
|
|
57
|
-
const atom = Atom(["first"]);
|
|
83
|
+
const atom = Atom<string[]>(["first"]);
|
|
84
|
+
// this is not allowed
|
|
85
|
+
// atom.push(1);
|
|
58
86
|
expect(atom instanceof ArrayAtom).toBe(true);
|
|
59
87
|
expect(atom.value().length).toBe(1);
|
|
60
88
|
expect(atom.value()[0]).toBe("first");
|
|
@@ -65,7 +93,8 @@ test("Array Atom values test", () => {
|
|
|
65
93
|
});
|
|
66
94
|
|
|
67
95
|
test("Array Atom get index test", () => {
|
|
68
|
-
const atom = Atom(["first"]);
|
|
96
|
+
const atom = Atom<string[]>(["first"]);
|
|
97
|
+
|
|
69
98
|
expect(atom instanceof ArrayAtom).toBe(true);
|
|
70
99
|
expect(atom.value().length).toBe(1);
|
|
71
100
|
expect(atom.get(0)).toBe("first");
|
|
@@ -80,23 +109,38 @@ test("Test native pipe", () => {
|
|
|
80
109
|
expect(atom instanceof BaseAtom).toBe(true);
|
|
81
110
|
expect(atom.value()).toBe(3);
|
|
82
111
|
|
|
83
|
-
const
|
|
84
|
-
expect(
|
|
85
|
-
expect(
|
|
112
|
+
const derivedAtom = atom.pipe(map((x) => x * x));
|
|
113
|
+
expect(derivedAtom instanceof ReadOnlyAtom).toBe(true);
|
|
114
|
+
expect(derivedAtom).not.toHaveProperty("set");
|
|
86
115
|
|
|
87
|
-
expect(
|
|
116
|
+
expect(derivedAtom.value()).toBe(9);
|
|
88
117
|
});
|
|
89
118
|
|
|
90
|
-
test("Test
|
|
119
|
+
test("Test derive", () => {
|
|
91
120
|
const atom = Atom(3);
|
|
92
121
|
expect(atom instanceof BaseAtom).toBe(true);
|
|
93
122
|
expect(atom.value()).toBe(3);
|
|
94
123
|
|
|
95
|
-
const
|
|
96
|
-
expect(
|
|
97
|
-
expect(
|
|
124
|
+
const derivedAtom = atom.derive((x) => x * x);
|
|
125
|
+
expect(derivedAtom instanceof ReadOnlyAtom).toBe(true);
|
|
126
|
+
expect(derivedAtom).not.toHaveProperty("set");
|
|
98
127
|
|
|
99
|
-
expect(
|
|
128
|
+
expect(derivedAtom.value()).toBe(9);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("Test derive update", () => {
|
|
132
|
+
const atom = Atom(3);
|
|
133
|
+
expect(atom instanceof BaseAtom).toBe(true);
|
|
134
|
+
expect(atom.value()).toBe(3);
|
|
135
|
+
|
|
136
|
+
const derivedAtom = atom.derive((x) => x * x);
|
|
137
|
+
expect(derivedAtom instanceof ReadOnlyAtom).toBe(true);
|
|
138
|
+
expect(derivedAtom).not.toHaveProperty("set");
|
|
139
|
+
|
|
140
|
+
expect(derivedAtom.value()).toBe(9);
|
|
141
|
+
|
|
142
|
+
atom.set(4);
|
|
143
|
+
expect(derivedAtom.value()).toBe(16);
|
|
100
144
|
});
|
|
101
145
|
|
|
102
146
|
test("Test combine", () => {
|
|
@@ -106,20 +150,199 @@ test("Test combine", () => {
|
|
|
106
150
|
c: { name: "c" },
|
|
107
151
|
});
|
|
108
152
|
const ids = Atom<string[]>(["a", "b", "c"]);
|
|
109
|
-
const combined = Atom.combine(normalizedData, ids).
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
expect(combined).not.toHaveProperty("push");
|
|
153
|
+
const combined = Atom.combine(normalizedData, ids).derive(([normed, ids]) => {
|
|
154
|
+
return ids.map((id) => normed[id]);
|
|
155
|
+
});
|
|
156
|
+
expect(combined).not.toHaveProperty("set");
|
|
115
157
|
|
|
116
158
|
expect(combined instanceof ReadOnlyAtom).toBe(true);
|
|
117
159
|
|
|
118
160
|
expect(combined instanceof ReadOnlyAtom).toBe(true);
|
|
119
|
-
expect(combined).not.toHaveProperty("
|
|
161
|
+
expect(combined).not.toHaveProperty("set");
|
|
120
162
|
const combinedValue = combined.value();
|
|
121
163
|
expect(combinedValue.length).toBe(3);
|
|
122
164
|
expect(combinedValue[0].name).toBe("a");
|
|
123
165
|
expect(combinedValue[1].name).toBe("b");
|
|
124
166
|
expect(combinedValue[2].name).toBe("c");
|
|
125
167
|
});
|
|
168
|
+
|
|
169
|
+
test("Test combine example", () => {
|
|
170
|
+
const pets$ = Atom<{ [name: string]: { type: "dog" | "cat"; age: number } }>({
|
|
171
|
+
spot: { type: "dog", age: 5 },
|
|
172
|
+
fido: { type: "dog", age: 3 },
|
|
173
|
+
tabby: { type: "cat", age: 12 },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const people$ = Atom<{ [name: string]: { pets: string[] } }>({
|
|
177
|
+
fred: { pets: [] },
|
|
178
|
+
mary: { pets: ["spot", "fido"] },
|
|
179
|
+
cam: { pets: ["tabby"] },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const mary$ = Atom.combine(pets$, people$.select("mary")).derive(
|
|
183
|
+
([pets, mary]) => {
|
|
184
|
+
return {
|
|
185
|
+
...mary,
|
|
186
|
+
pets: mary.pets.map((petName) => pets[petName]),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
{
|
|
191
|
+
mary: {
|
|
192
|
+
pets: [
|
|
193
|
+
{
|
|
194
|
+
name: "spot",
|
|
195
|
+
type: "dog",
|
|
196
|
+
age: 5,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const pets = mary$.select("pets").get(0);
|
|
203
|
+
expect(mary$.select("pets").value().length).toBe(2);
|
|
204
|
+
expect(mary$.select("pets").get(0)).toHaveProperty("type");
|
|
205
|
+
expect(mary$.select("pets").get(0)).toHaveProperty("age");
|
|
206
|
+
expect(mary$.select("pets").get(0).type).toBe("dog");
|
|
207
|
+
expect(mary$.select("pets").get(0).age).toBe(5);
|
|
208
|
+
expect(mary$.select("pets").get(1)).toHaveProperty("type");
|
|
209
|
+
expect(mary$.select("pets").get(1)).toHaveProperty("age");
|
|
210
|
+
expect(mary$.select("pets").get(1).type).toBe("dog");
|
|
211
|
+
expect(mary$.select("pets").get(1).age).toBe(3);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("Test select (simple)", () => {
|
|
215
|
+
enum TEST_ENUM {
|
|
216
|
+
a = "weoifj",
|
|
217
|
+
b = "oh",
|
|
218
|
+
c = "ohoij",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// test types
|
|
222
|
+
const enumTest = Atom(TEST_ENUM.a);
|
|
223
|
+
|
|
224
|
+
const arrayTest = Atom([1, 2, 34]);
|
|
225
|
+
const arrayTest2 = Atom([{ a: "jwoi" }, 2, 34]);
|
|
226
|
+
|
|
227
|
+
const arrayAtom = Atom<{ [key: string]: number[] }>({
|
|
228
|
+
a: [0, 1, 2],
|
|
229
|
+
b: [1, 3, 4],
|
|
230
|
+
c: [5, 3, 4],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const primitiveNum = new BehaviorSubject(4);
|
|
234
|
+
const primitiveNumAtom = Atom<number>(primitiveNum);
|
|
235
|
+
|
|
236
|
+
const primitiveStr = new BehaviorSubject("a");
|
|
237
|
+
const primitiveStringAtom = Atom<string>(primitiveStr);
|
|
238
|
+
|
|
239
|
+
const arrayObs = new BehaviorSubject(["a"]);
|
|
240
|
+
const arrayObsAtom = Atom<string[]>(arrayObs);
|
|
241
|
+
|
|
242
|
+
const objObs = new BehaviorSubject({ a: 1 });
|
|
243
|
+
const objObsAtom2 = Atom(objObs);
|
|
244
|
+
const objObsAtom1 = Atom<{ [id: string]: number }>(objObs);
|
|
245
|
+
|
|
246
|
+
const stringData = Atom<{
|
|
247
|
+
[key: string]: { [key: string]: string };
|
|
248
|
+
}>({
|
|
249
|
+
a: { a: "a" },
|
|
250
|
+
b: { b: "b" },
|
|
251
|
+
c: { c: "c" },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const selectedString: ObjectAtom<{ [key: string]: string }> =
|
|
255
|
+
stringData.select("b");
|
|
256
|
+
|
|
257
|
+
const normalizedOptionalStringData = Atom<{
|
|
258
|
+
[key: string]: { [key in "a" | "b" | "c"]?: string };
|
|
259
|
+
}>({
|
|
260
|
+
a: { a: "a" },
|
|
261
|
+
b: { b: "b" },
|
|
262
|
+
c: { c: "c" },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const selectedOptionalString = normalizedOptionalStringData.select("b");
|
|
266
|
+
|
|
267
|
+
const normalizedEnumData = Atom<{
|
|
268
|
+
[key: string]: { [key in TEST_ENUM]?: string };
|
|
269
|
+
}>({
|
|
270
|
+
a: { [TEST_ENUM.a]: "a" },
|
|
271
|
+
b: { [TEST_ENUM.b]: "b" },
|
|
272
|
+
c: { [TEST_ENUM.c]: "c" },
|
|
273
|
+
});
|
|
274
|
+
const selected = normalizedEnumData.select("b");
|
|
275
|
+
const selected2 = normalizedEnumData.select("a");
|
|
276
|
+
const selectedArray = arrayAtom.select("b");
|
|
277
|
+
|
|
278
|
+
// THIS IS HTE LAST ONE THT STILL OES NOTWOK
|
|
279
|
+
const selectedArr = arrayAtom.select("a");
|
|
280
|
+
|
|
281
|
+
const sel3 = objObsAtom2.select("a");
|
|
282
|
+
|
|
283
|
+
expect(selected instanceof ObjectAtom).toBe(true);
|
|
284
|
+
|
|
285
|
+
expect(selected instanceof ObjectAtom).toBe(true);
|
|
286
|
+
|
|
287
|
+
// const combined = Atom.combine(normalizedData, ids).derive(([normed, ids]) => {
|
|
288
|
+
// return ids.map((id) => normed[id]);
|
|
289
|
+
// });
|
|
290
|
+
//
|
|
291
|
+
// expect(combined).not.toHaveProperty("push");
|
|
292
|
+
//
|
|
293
|
+
// expect(combined instanceof ReadOnlyAtom).toBe(true);
|
|
294
|
+
//
|
|
295
|
+
// expect(combined instanceof ReadOnlyAtom).toBe(true);
|
|
296
|
+
// expect(combined).not.toHaveProperty("push");
|
|
297
|
+
// const combinedValue = combined.value();
|
|
298
|
+
// expect(combinedValue.length).toBe(3);
|
|
299
|
+
// expect(combinedValue[0].name).toBe("a");
|
|
300
|
+
// expect(combinedValue[1].name).toBe("b");
|
|
301
|
+
// expect(combinedValue[2].name).toBe("c");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("Test select (nested objects)", () => {
|
|
305
|
+
const nestedData = Atom<{
|
|
306
|
+
[key: string]: {
|
|
307
|
+
nickname: string;
|
|
308
|
+
education: {
|
|
309
|
+
school: string;
|
|
310
|
+
graduation: number;
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
}>({
|
|
314
|
+
stacy: {
|
|
315
|
+
nickname: "stace",
|
|
316
|
+
education: {
|
|
317
|
+
school: "Penn",
|
|
318
|
+
graduation: 2014,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
annie: {
|
|
322
|
+
nickname: "ann",
|
|
323
|
+
education: {
|
|
324
|
+
school: "Brown",
|
|
325
|
+
graduation: 2015,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
prabhu: {
|
|
329
|
+
nickname: "prab",
|
|
330
|
+
education: {
|
|
331
|
+
school: "MIT",
|
|
332
|
+
graduation: 2016,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
const stacy = nestedData.select("stacy");
|
|
337
|
+
const stacySchool = nestedData.select("stacy").select("education");
|
|
338
|
+
|
|
339
|
+
expect(stacy.get("nickname")).toBe("stace");
|
|
340
|
+
expect(stacySchool.get("school")).toBe("Penn");
|
|
341
|
+
expect(stacySchool.get("graduation")).toBe(2014);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/*
|
|
345
|
+
* TODO:
|
|
346
|
+
* - test react hooks
|
|
347
|
+
* - test subscriptions
|
|
348
|
+
*/
|
package/tests/sample.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export class ReadOnlyClass<T> {
|
|
2
|
+
value: T;
|
|
3
|
+
constructor(_value: T | Wrapper<T>) {
|
|
4
|
+
if (isWrapper(_value)) {
|
|
5
|
+
} else {
|
|
6
|
+
this.value = _value;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ArrayClass<T> extends ReadOnlyClass<T[]> {}
|
|
12
|
+
|
|
13
|
+
export class BaseClass<T> extends ReadOnlyClass<T> {}
|
|
14
|
+
|
|
15
|
+
export class ObjectClass<
|
|
16
|
+
K extends string | number | symbol,
|
|
17
|
+
V,
|
|
18
|
+
T extends {
|
|
19
|
+
[key in K]: V;
|
|
20
|
+
} = {
|
|
21
|
+
[key in K]: V;
|
|
22
|
+
}
|
|
23
|
+
> extends ReadOnlyClass<T> {
|
|
24
|
+
select(
|
|
25
|
+
key: K
|
|
26
|
+
): T[K] extends { [key: string]: infer V } ? ObjectClass<K, V> : BaseClass<V>;
|
|
27
|
+
select(key: K) {
|
|
28
|
+
const newObs = new Wrapper(this.value[key]);
|
|
29
|
+
return Class(newObs);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Wrapper<T> {
|
|
34
|
+
val: T;
|
|
35
|
+
constructor(val: T) {
|
|
36
|
+
this.val = val;
|
|
37
|
+
}
|
|
38
|
+
getValue(): T {
|
|
39
|
+
return this.val;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isWrapper(val: any): val is Wrapper<unknown> {
|
|
44
|
+
if (val instanceof Wrapper) {
|
|
45
|
+
return true;
|
|
46
|
+
} else {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// wrapper type (primitive)
|
|
52
|
+
export function Class<T>(
|
|
53
|
+
value: T extends { [key: string]: infer V } | any[] ? never : Wrapper<T>
|
|
54
|
+
): BaseClass<T>;
|
|
55
|
+
|
|
56
|
+
// wrapper<array> type
|
|
57
|
+
export function Class<T extends any[]>(
|
|
58
|
+
value: Wrapper<T>
|
|
59
|
+
): ArrayClass<T[number]>;
|
|
60
|
+
|
|
61
|
+
// wrapper<object> type
|
|
62
|
+
export function Class<T>(
|
|
63
|
+
value: T extends {
|
|
64
|
+
[key in keyof T]: infer V;
|
|
65
|
+
}
|
|
66
|
+
? Wrapper<T>
|
|
67
|
+
: never
|
|
68
|
+
): ObjectClass<keyof T, T[keyof T], T>;
|
|
69
|
+
|
|
70
|
+
// array type
|
|
71
|
+
export function Class<T extends any[]>(value: T): ArrayClass<T[number]>;
|
|
72
|
+
|
|
73
|
+
// object type
|
|
74
|
+
export function Class<T extends { [key: string]: T[keyof T] }>(
|
|
75
|
+
value: T
|
|
76
|
+
): ObjectClass<keyof T, T[keyof T]>;
|
|
77
|
+
|
|
78
|
+
// primitive type
|
|
79
|
+
export function Class<T>(value: T): BaseClass<T>;
|
|
80
|
+
|
|
81
|
+
// function definition
|
|
82
|
+
export function Class<T>(_value: T) {
|
|
83
|
+
let cls;
|
|
84
|
+
if (Array.isArray(_value)) {
|
|
85
|
+
cls = new ArrayClass<T>(_value); // For arrays
|
|
86
|
+
} else if (typeof _value === "object" && _value !== null) {
|
|
87
|
+
cls = new ObjectClass<keyof T, T[keyof T]>(_value); // For objects
|
|
88
|
+
} else {
|
|
89
|
+
cls = new BaseClass(_value); // For other types
|
|
90
|
+
}
|
|
91
|
+
return cls;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const a = Class<{ [key: string]: { name: string } }>({
|
|
95
|
+
a: { name: "a" },
|
|
96
|
+
b: { name: "b" },
|
|
97
|
+
c: { name: "c" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// this works
|
|
101
|
+
const numbWrapper = new Wrapper(1);
|
|
102
|
+
const numbClass = Class(numbWrapper); // BaseClass<number>
|
|
103
|
+
|
|
104
|
+
// this works
|
|
105
|
+
const arrayWrapper = new Wrapper(["a"]);
|
|
106
|
+
const arrayWrapperClass = Class<string[]>(arrayWrapper); // ArrayClass<number>
|
|
107
|
+
|
|
108
|
+
// this works
|
|
109
|
+
const objObs = new Wrapper({ a: 1 });
|
|
110
|
+
const objObsClass2 = Class(objObs); // ObjectClass<'a', number, { a: number }>
|
|
111
|
+
|
|
112
|
+
// this does not work.
|
|
113
|
+
const selectedItem = a.select("b"); // BaseClass<{name: string}>
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* Why is this of type:
|
|
117
|
+
* const selectedItem: BaseClass<{
|
|
118
|
+
* name: string;
|
|
119
|
+
* }>
|
|
120
|
+
*
|
|
121
|
+
* instead of:
|
|
122
|
+
* const selectedItem: BaseClass<ObjectClass<'name', string>
|
|
123
|
+
*/
|