Skip to content

Commit

Permalink
docs(core): types union fix
Browse files Browse the repository at this point in the history
  • Loading branch information
artalar committed Nov 12, 2024
1 parent f4d1b84 commit f1854c0
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 84 deletions.
118 changes: 76 additions & 42 deletions docs/src/content/docs/package/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down Expand Up @@ -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<number>
```

Expand Down Expand Up @@ -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 `<T extends Atom>(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))
```
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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, () => {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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<string> | AtomMut<number>' 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: {
<T extends Atom>(anAtom: T): T extends Atom<infer State> ? State : never
<T>(anAtom: Atom<T>): T
<T>(
cb: Fn<
[
read: Fn<[proto: AtomProto], AtomCache<any> | undefined>,
// this is `actualize` function and
// the types intentionally awkward
// coz it only for internal usage
fn?: Fn,
],
T
>,
): T
}
// @ts-expect-error
spy?: {
<T extends Atom>(anAtom: T): T extends Atom<infer State> ? State : never
<T>(anAtom: Atom<T>): T
<Params extends any[] = any[], Payload = any>(
anAction: Action<Params, Payload>,
cb: Fn<[call: { params: Params; payload: Payload }]>,
): void
<T>(atom: Atom<T>, cb: Fn<[newState: T, prevState: undefined | T]>): void
}

schedule<T = void>(cb: Fn<[Ctx], T>, step?: -1 | 0 | 1 | 2): Promise<Awaited<T>>

subscribe<T>(atom: Atom<T>, 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
```
118 changes: 76 additions & 42 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down Expand Up @@ -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<number>
```

Expand Down Expand Up @@ -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 `<T extends Atom>(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))
```
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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, () => {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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<string> | AtomMut<number>' 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: {
<T extends Atom>(anAtom: T): T extends Atom<infer State> ? State : never
<T>(anAtom: Atom<T>): T
<T>(
cb: Fn<
[
read: Fn<[proto: AtomProto], AtomCache<any> | undefined>,
// this is `actualize` function and
// the types intentionally awkward
// coz it only for internal usage
fn?: Fn,
],
T
>,
): T
}
// @ts-expect-error
spy?: {
<T extends Atom>(anAtom: T): T extends Atom<infer State> ? State : never
<T>(anAtom: Atom<T>): T
<Params extends any[] = any[], Payload = any>(
anAction: Action<Params, Payload>,
cb: Fn<[call: { params: Params; payload: Payload }]>,
): void
<T>(atom: Atom<T>, cb: Fn<[newState: T, prevState: undefined | T]>): void
}

schedule<T = void>(cb: Fn<[Ctx], T>, step?: -1 | 0 | 1 | 2): Promise<Awaited<T>>

subscribe<T>(atom: Atom<T>, 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
```

0 comments on commit f1854c0

Please sign in to comment.