diff --git a/docs/src/content/docs/package/core.md b/docs/src/content/docs/package/core.md index 3f588f778..450546ff2 100644 --- a/docs/src/content/docs/package/core.md +++ b/docs/src/content/docs/package/core.md @@ -90,9 +90,7 @@ const fetchGoods = action(async (ctx, search: string) => { // schedule side-effects // which will be called after successful execution of all computations - const goods = await ctx.schedule(() => - fetch(`/api/goods?search=${search}`).then((r) => r.json()), - ) + const goods = await ctx.schedule(() => fetch(`/api/goods?search=${search}`).then((r) => r.json())) // use `batch` to prevent glitches and extra effects. batch(ctx, () => { @@ -207,10 +205,7 @@ You could create a computed derived atom by passing a function to `atom`. The fi > **Note to TypeScript users**: It is impossible to describe the reducer type with an optional generic state argument, which is returned from the function. If you use the second `state` argument, you should define its type; do not rely on the return type. ```ts -const isCountEvenAtom = atom( - (ctx) => ctx.spy(countAtom) % 2 === 0, - 'isCountEven', -) +const isCountEvenAtom = atom((ctx) => ctx.spy(countAtom) % 2 === 0, 'isCountEven') // isCountEvenAtom: Atom ``` @@ -276,9 +271,7 @@ export const currencyAtom = atom((ctx, state?: string) => { Pipe is a general chain helper, it applies an operator to the atom to map it to another thing. Classic operator interface is `(options?: any) => (anAtom: T) => aNewThing`. The main reason is a readable and type-safe way to apply decorators. ```ts -const countAtom = atom(0).pipe( - withInit(() => localStorage.getItem('COUNT') ?? 0), -) +const countAtom = atom(0).pipe(withInit(() => localStorage.getItem('COUNT') ?? 0)) // equals to const countAtom = withInit(() => localStorage.getItem('COUNT') ?? 0)(atom(0)) ``` @@ -380,7 +373,6 @@ doSome.onCall((ctx, payload, params) => { }) ``` - ## createCtx API A context creation function accepts a few optional parameters that you probably won't want to change in regular use. However, it might be useful for testing and some rare production needs. @@ -485,10 +477,7 @@ import { action, atom, batch } from '@reatom/core' export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const fetchUser = action(async (ctx, id: string) => { const user = await ctx.schedule(() => api.getUser(id)) firstNameAtom(ctx, user.firstName) @@ -511,10 +500,7 @@ import { action, atom, batch } from '@reatom/core' export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const fetchUser = action(async (ctx, id: string) => { const user = await ctx.schedule(() => api.getUser(id)) batch(ctx, () => { @@ -539,10 +525,7 @@ import { action, atom, batch } from '@reatom/core' export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const saveUser = action((ctx, firstName: string, lastName: string) => { firstNameAtom(ctx, firstName) lastNameAtom(ctx, lastName) @@ -571,10 +554,7 @@ import { action, atom, batch } from '@reatom/core' export const isUserLoadingAtom = atom(false, 'isUserLoadingAtom') export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const saveUser = action((ctx, firstName: string, lastName: string) => { firstNameAtom(ctx, firstName) lastNameAtom(ctx, lastName) @@ -599,21 +579,15 @@ import { action, atom, batch } from '@reatom/core' export const isUserLoadingAtom = atom(false, 'isUserLoadingAtom') export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const saveUser = action((ctx, firstName: string, lastName: string) => { firstNameAtom(ctx, firstName) lastNameAtom(ctx, lastName) }, 'saveUser') -export const resolveFetchUser = action( - (ctx, firstName: string, lastName: string) => { - saveUser(ctx, firstName, firstName) - isUserLoadingAtom(ctx, false) - }, - 'resolveFetchUser', -) +export const resolveFetchUser = action((ctx, firstName: string, lastName: string) => { + saveUser(ctx, firstName, firstName) + isUserLoadingAtom(ctx, false) +}, 'resolveFetchUser') export const fetchUser = action(async (ctx, id: string) => { isUserLoadingAtom(ctx, true) try { @@ -633,10 +607,7 @@ import { action, atom, batch } from '@reatom/core' export const isUserLoadingAtom = atom(false, 'isUserLoadingAtom') export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const fetchUser = action(async (ctx, id: string) => { isUserLoadingAtom(ctx, true) try { @@ -651,3 +622,66 @@ export const fetchUser = action(async (ctx, id: string) => { } }, 'fetchUser') ``` + +## TypeScript + +### Unions + +If you need to "get" or "spy" an atoms with a different types you will got an error in a generic inference. + +```ts +const nameAtom = atom('') +const ageAtom = atom(0) +const valuesAtom = atom((ctx) => [nameAtom, ageAtom].map((a) => ctx.spy(a))) +// Error: Argument of type 'AtomMut | AtomMut' is not assignable to parameter of type... +``` + +To fix it, you can add this declarations modification. We don't include it to the v3 version of the core package, as it can break the behavior of an existed code in very rare cases. + +```ts +import { Atom, Fn, AtomProto, AtomCache, Action, Unsubscribe, Logs } from '@reatom/core' + +declare module '@reatom/core' { + export interface Ctx { + // @ts-expect-error + get: { + (anAtom: T): T extends Atom ? State : never + (anAtom: Atom): T + ( + cb: Fn< + [ + read: Fn<[proto: AtomProto], AtomCache | undefined>, + // this is `actualize` function and + // the types intentionally awkward + // coz it only for internal usage + fn?: Fn, + ], + T + >, + ): T + } + // @ts-expect-error + spy?: { + (anAtom: T): T extends Atom ? State : never + (anAtom: Atom): T + ( + anAction: Action, + cb: Fn<[call: { params: Params; payload: Payload }]>, + ): void + (atom: Atom, cb: Fn<[newState: T, prevState: undefined | T]>): void + } + + schedule(cb: Fn<[Ctx], T>, step?: -1 | 0 | 1 | 2): Promise> + + subscribe(atom: Atom, cb: Fn<[T]>): Unsubscribe + subscribe(cb: Fn<[patches: Logs, error?: Error]>): Unsubscribe + + cause: AtomCache + } +} + +const nameAtom = atom('') +const ageAtom = atom(0) +const valuesAtom = atom((ctx) => [nameAtom, ageAtom].map((a) => ctx.spy(a))) +// all fine +``` diff --git a/packages/core/README.md b/packages/core/README.md index 854d0e6a9..3324d6de5 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -81,9 +81,7 @@ const fetchGoods = action(async (ctx, search: string) => { // schedule side-effects // which will be called after successful execution of all computations - const goods = await ctx.schedule(() => - fetch(`/api/goods?search=${search}`).then((r) => r.json()), - ) + const goods = await ctx.schedule(() => fetch(`/api/goods?search=${search}`).then((r) => r.json())) // use `batch` to prevent glitches and extra effects. batch(ctx, () => { @@ -198,10 +196,7 @@ You could create a computed derived atom by passing a function to `atom`. The fi > **Note to TypeScript users**: It is impossible to describe the reducer type with an optional generic state argument, which is returned from the function. If you use the second `state` argument, you should define its type; do not rely on the return type. ```ts -const isCountEvenAtom = atom( - (ctx) => ctx.spy(countAtom) % 2 === 0, - 'isCountEven', -) +const isCountEvenAtom = atom((ctx) => ctx.spy(countAtom) % 2 === 0, 'isCountEven') // isCountEvenAtom: Atom ``` @@ -267,9 +262,7 @@ export const currencyAtom = atom((ctx, state?: string) => { Pipe is a general chain helper, it applies an operator to the atom to map it to another thing. Classic operator interface is `(options?: any) => (anAtom: T) => aNewThing`. The main reason is a readable and type-safe way to apply decorators. ```ts -const countAtom = atom(0).pipe( - withInit(() => localStorage.getItem('COUNT') ?? 0), -) +const countAtom = atom(0).pipe(withInit(() => localStorage.getItem('COUNT') ?? 0)) // equals to const countAtom = withInit(() => localStorage.getItem('COUNT') ?? 0)(atom(0)) ``` @@ -371,7 +364,6 @@ doSome.onCall((ctx, payload, params) => { }) ``` - ## createCtx API A context creation function accepts a few optional parameters that you probably won't want to change in regular use. However, it might be useful for testing and some rare production needs. @@ -476,10 +468,7 @@ import { action, atom, batch } from '@reatom/core' export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const fetchUser = action(async (ctx, id: string) => { const user = await ctx.schedule(() => api.getUser(id)) firstNameAtom(ctx, user.firstName) @@ -502,10 +491,7 @@ import { action, atom, batch } from '@reatom/core' export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const fetchUser = action(async (ctx, id: string) => { const user = await ctx.schedule(() => api.getUser(id)) batch(ctx, () => { @@ -530,10 +516,7 @@ import { action, atom, batch } from '@reatom/core' export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const saveUser = action((ctx, firstName: string, lastName: string) => { firstNameAtom(ctx, firstName) lastNameAtom(ctx, lastName) @@ -562,10 +545,7 @@ import { action, atom, batch } from '@reatom/core' export const isUserLoadingAtom = atom(false, 'isUserLoadingAtom') export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const saveUser = action((ctx, firstName: string, lastName: string) => { firstNameAtom(ctx, firstName) lastNameAtom(ctx, lastName) @@ -590,21 +570,15 @@ import { action, atom, batch } from '@reatom/core' export const isUserLoadingAtom = atom(false, 'isUserLoadingAtom') export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const saveUser = action((ctx, firstName: string, lastName: string) => { firstNameAtom(ctx, firstName) lastNameAtom(ctx, lastName) }, 'saveUser') -export const resolveFetchUser = action( - (ctx, firstName: string, lastName: string) => { - saveUser(ctx, firstName, firstName) - isUserLoadingAtom(ctx, false) - }, - 'resolveFetchUser', -) +export const resolveFetchUser = action((ctx, firstName: string, lastName: string) => { + saveUser(ctx, firstName, firstName) + isUserLoadingAtom(ctx, false) +}, 'resolveFetchUser') export const fetchUser = action(async (ctx, id: string) => { isUserLoadingAtom(ctx, true) try { @@ -624,10 +598,7 @@ import { action, atom, batch } from '@reatom/core' export const isUserLoadingAtom = atom(false, 'isUserLoadingAtom') export const firstNameAtom = atom('', 'firstNameAtom') export const lastNameAtom = atom('', 'lastNameAtom') -export const fullNameAtom = atom( - (ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, - 'fullNameAtom', -) +export const fullNameAtom = atom((ctx) => `${ctx.spy(firstNameAtom)} ${ctx.spy(lastNameAtom)}`, 'fullNameAtom') export const fetchUser = action(async (ctx, id: string) => { isUserLoadingAtom(ctx, true) try { @@ -642,3 +613,66 @@ export const fetchUser = action(async (ctx, id: string) => { } }, 'fetchUser') ``` + +## TypeScript + +### Unions + +If you need to "get" or "spy" an atoms with a different types you will got an error in a generic inference. + +```ts +const nameAtom = atom('') +const ageAtom = atom(0) +const valuesAtom = atom((ctx) => [nameAtom, ageAtom].map((a) => ctx.spy(a))) +// Error: Argument of type 'AtomMut | AtomMut' is not assignable to parameter of type... +``` + +To fix it, you can add this declarations modification. We don't include it to the v3 version of the core package, as it can break the behavior of an existed code in very rare cases. + +```ts +import { Atom, Fn, AtomProto, AtomCache, Action, Unsubscribe, Logs } from '@reatom/core' + +declare module '@reatom/core' { + export interface Ctx { + // @ts-expect-error + get: { + (anAtom: T): T extends Atom ? State : never + (anAtom: Atom): T + ( + cb: Fn< + [ + read: Fn<[proto: AtomProto], AtomCache | undefined>, + // this is `actualize` function and + // the types intentionally awkward + // coz it only for internal usage + fn?: Fn, + ], + T + >, + ): T + } + // @ts-expect-error + spy?: { + (anAtom: T): T extends Atom ? State : never + (anAtom: Atom): T + ( + anAction: Action, + cb: Fn<[call: { params: Params; payload: Payload }]>, + ): void + (atom: Atom, cb: Fn<[newState: T, prevState: undefined | T]>): void + } + + schedule(cb: Fn<[Ctx], T>, step?: -1 | 0 | 1 | 2): Promise> + + subscribe(atom: Atom, cb: Fn<[T]>): Unsubscribe + subscribe(cb: Fn<[patches: Logs, error?: Error]>): Unsubscribe + + cause: AtomCache + } +} + +const nameAtom = atom('') +const ageAtom = atom(0) +const valuesAtom = atom((ctx) => [nameAtom, ageAtom].map((a) => ctx.spy(a))) +// all fine +```