Repository: effector/effector (ts)

Issue:
Deprecate `undefined` as a magic skip update value and allow it as a normal value
# Background

There is an ancient feature in the `effector` - when `undefined` is returned from some reducer, it reads as `skip update` command and store does not update its state.
The idea was to make every reducer a kind of `filterMap` thing, which, in theory, can be handy sometimes, but in practice this feature is basically almost not used at all.

Its have few consequences:
- `undefined` basically cannot be used as a state of the store. `createStore(undefined)` will throw.
- Mapped stores are becoming "kind of stateful" - they do not have direct access to previous state, but still can do basically `return previousState` via `undefined`

Basically, now it is a outdated and confusing design desicion, which is to be removed.

# Explanation

As per effector's Releases Policy, every breaking change must be moved through deprecation period first. 
But just deprecating it with a warning is not an option, as this way we basically will lose the possibility to use `undefined` as a value in stores in any way - basically it will become sort of UB, which is weird.

This means, we need a graceful way to _change_ the semantics of `undefined` usage in reducers.

To do so, we need to introduce a separate configuration, which will set this behavior to specific option.

After that, in the effector 24, we will be able to change the default setting and deprecate `skipVoid: true`.

And in the effector 25 finally get rid of this feature at all.

## Examples

### Derived stores

In effector 23 this code will show a warning, requiring the user to explicitly set `skipVoid` setting in the configuration, while keeping the old `filterMap`-like behavior.

#### No config

Should skip update and show a warning, requiring the user to explicitly provide the configuration
```ts
$store.map(s => s ? 42 : undefined)
```

#### Explicit `skipVoid: false`

This code will turn off `filterMap`-like behavoir and will allow `undefined` to pass further **as a value**

```ts
$store.map(s => s ? 42 : undefined, { skipVoid: false })
```

#### Explicit `skipVoid: true`

Will silenece the warning and keep current behaviour (skip of update)
```ts
$store.map(s => s ? 42 : undefined, { skipVoid: true })
```

#### Not affected case

The code, which does not use `undefined` in any way, will not require any configuration
```ts
$store.map(s => s ? 42 : null)
```

The same applies to `combine`

### Base stores

In effector 23 `createStore` declaration with undefined as a base value should still throw, just like all versions before, but this time `undefined` as a store value can be allowed as manual `{ skipVoid: false }` configuration. 

Also, in reducers and samples, that are going to write undefined into store, warning, similiar to the one of the combine and map, should be shown

#### Not affected case

Those stores, which are not using undefined in any way, are not affected by this change

```ts
const $store = createStore<SomeObject | null>(null)
```

#### Store declaration with void or undefined

Throws immediatly with message like "To allow undefined value in store add { skipVoid: false } into the config
```ts
const $storeA = createStore()
const $storeB = createStore(undefined)
```

#### Writing undefined into store

Skips update and shows warning, requiring the user to explicitly set `skipVoid` setting in the store configuration OR return previous state to explicitly skip update

```ts
$store.on(event, () => undefined)
$store.on(event, () => {
 // nothing
})

sample({
 clock: event,
 fn: () => {
   // nothing
 },
 target: $store,
})
```

#### Explicit `skipVoid: false`

Explicit `skipVoid: false` allows to use `undefined` as a value in this store in all cases

```ts
const $store = createStore(undefined, {skipVoid: false})

$store.on(event, () => undefined)
$store.on(event, () => {
 // nothing
})

sample({
 clock: event,
 fn: () => {
   // nothing
 },
 target: $store,
})
```

#### Explicit `skipVoid: true`

Keeps current behaviour AND mutes all the warnings




Pull request:
Void skip deprecation
Closes #920 #828 

Patch:
diff --git a/beta/src/content/docs/en/api/effector/Store.md b/beta/src/content/docs/en/api/effector/Store.md
index 698c94c4..a13ea10e 100644
--- a/beta/src/content/docs/en/api/effector/Store.md
+++ b/beta/src/content/docs/en/api/effector/Store.md
@@ -19,11 +19,6 @@ be [derived](#store-derived).
 
 ## `map(fn)` {#map-fn}
 
-:::info
-Since [effector 21.8.0](https://github.com/effector/effector/releases/tag/effector%4021.8.0) the second argument of `fn`
-and `firstState` are deprecated, use [updateFilter](/en/api/effector/createStore) or explicit `createStore` instead.
-:::
-
 Creates a derived store. It will call a provided function with the state, when the original store updates, and will use
 the result to update the derived store
 
@@ -42,6 +37,9 @@ const $second = $first.map(fn);
 
 If the function returns an old state or if it returns `undefined`, the new store will not be updated.
 
+2. `config` (_Object_): Optional configuration
+   - `skipVoid`: (_boolean_): Flag to control, how specifically store should handle `undefined` value _(since `effector 23.0.0`)_. If set to `false` - store will use `undefined` as a value. If set to `true` (deprecated), store will read `undefined` as a "skip update" command and will do nothing.
+
 [Should be **pure**](/en/explanation/glossary#purity)
 
 ### Throws {#map-fn-throws}
@@ -113,6 +111,12 @@ changed("hello world");
 
 [Try it](https://share.effector.dev/XGKGMvpF)
 
+### Skip Void Example {#map-skip-void-example}
+
+```js
+const $length = $title.map((title) => title.length, {skipVoid: false});
+```
+
 ## `on(trigger, reducer)` {#on}
 
 Updates state when `trigger` is triggered by using [reducer](/en/explanation/glossary#reducer). For each trigger, the
diff --git a/beta/src/content/docs/en/api/effector/combine.md b/beta/src/content/docs/en/api/effector/combine.md
index d3db7ca6..771cefce 100644
--- a/beta/src/content/docs/en/api/effector/combine.md
+++ b/beta/src/content/docs/en/api/effector/combine.md
@@ -181,3 +181,14 @@ $withFn.watch(console.log);
 ```
 
 [Try it](https://share.effector.dev/XWk1lG4a)
+
+## Optional configuration {#combine-optional-configuration}
+
+All overloads of `combine` with `fn` provided are also supporting optional configuration object, wich includes:
+   - `skipVoid`: (_boolean_): Flag to control, how specifically store should handle `undefined` value _(since `effector 23.0.0`)_. If set to `false` - store will use `undefined` as a value. If set to `true` (deprecated), store will read `undefined` as a "skip update" command and will do nothing.
+
+#### Example
+
+```js
+const $withFn = combine($a, $b, (a, b) => a || b, {skipVoid: false});
+```
diff --git a/packages/effector/index.d.ts b/packages/effector/index.d.ts
index c2843f05..5a9df394 100644
--- a/packages/effector/index.d.ts
+++ b/packages/effector/index.d.ts
@@ -216,7 +216,7 @@ type InferValueFromTupleOfUnitTargetables<T extends Tuple<UnitTargetable<any>>>
 
 export interface Store<State> extends Unit<State> {
   kind: "store"
-  map<T>(fn: (state: State, lastState?: T) => T): Store<T>
+  map<T>(fn: (state: State, lastState?: T) => T, config?: {skipVoid?: boolean}): Store<T>
   /**
    * @deprecated second argument of `fn` and `firstState` are deprecated, use `updateFilter` or explicit `createStore` instead
    */
@@ -775,6 +775,7 @@ export function createEffect<Params, Done, Fail = Error>(config: {
 export function createStore<State, SerializedState extends Json = Json>(
   defaultState: State,
   config?: {
+    skipVoid?: boolean;
     name?: string;
     sid?: string
     updateFilter?: (update: State, current: State) => boolean
@@ -2900,7 +2901,7 @@ export function combine<State>(
  * @param fn transformer function, accepts a store value
  * @returns derived store updated upon changes in given one
  */
-export function combine<A, R>(source: Store<A>, fn: (source: A) => R): Store<R>
+export function combine<A, R>(source: Store<A>, fn: (source: A) => R, config?: {skipVoid: boolean}): Store<R>
 /**
  * Convert array of stores into derived store, transforming values using the function
  * @param tuple array of stores
@@ -2912,6 +2913,7 @@ export function combine<State extends Tuple, R>(
   fn: (
     tuple: {[K in keyof State]: State[K] extends Store<infer U> ? U : State[K]},
   ) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert object with stores into derived store, transforming values using the function
@@ -2924,6 +2926,7 @@ export function combine<State, R>(
   fn: (
     shape: {[K in keyof State]: State[K] extends Store<infer U> ? U : State[K]},
   ) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -2934,6 +2937,7 @@ export function combine<A, B, R>(
   a: A,
   b: B,
   fn: (a: CombVal<A>, b: CombVal<B>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -2945,6 +2949,7 @@ export function combine<A, B, C, R>(
   b: B,
   c: C,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -2957,6 +2962,7 @@ export function combine<A, B, C, D, R>(
   c: C,
   d: D,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -2970,6 +2976,7 @@ export function combine<A, B, C, D, E, R>(
   d: D,
   e: E,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -2984,6 +2991,7 @@ export function combine<A, B, C, D, E, F, R>(
   e: E,
   f: F,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>, f: CombVal<F>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -2999,6 +3007,7 @@ export function combine<A, B, C, D, E, F, G, R>(
   f: F,
   g: G,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>, f: CombVal<F>, g: CombVal<G>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -3018,6 +3027,7 @@ export function combine<A, B, C, D, E, F, G, H, R>(
   g: G,
   h: H,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>, f: CombVal<F>, g: CombVal<G>, h: CombVal<H>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -3038,6 +3048,7 @@ export function combine<A, B, C, D, E, F, G, H, I, R>(
   h: H,
   i: I,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>, f: CombVal<F>, g: CombVal<G>, h: CombVal<H>, i: CombVal<I>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -3059,6 +3070,7 @@ export function combine<A, B, C, D, E, F, G, H, I, J, R>(
   i: I,
   j: J,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>, f: CombVal<F>, g: CombVal<G>, h: CombVal<H>, i: CombVal<I>, j: CombVal<J>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores into derived store, transforming values using the function
@@ -3081,6 +3093,7 @@ export function combine<A, B, C, D, E, F, G, H, I, J, K, R>(
   j: J,
   k: K,
   fn: (a: CombVal<A>, b: CombVal<B>, c: CombVal<C>, d: CombVal<D>, e: CombVal<E>, f: CombVal<F>, g: CombVal<G>, h: CombVal<H>, i: CombVal<I>, j: CombVal<J>, k: CombVal<K>) => R,
+  config?: {skipVoid: boolean}
 ): Store<R>
 /**
  * Convert given stores to store with array which values updated upon changes in given ones
diff --git a/src/effector/combine.ts b/src/effector/combine.ts
index cbb60354..eccb976a 100644
--- a/src/effector/combine.ts
+++ b/src/effector/combine.ts
@@ -1,5 +1,5 @@
 import type {Store} from './unit.h'
-import {createStore} from './createUnit'
+import {createStore, requireExplicitSkipVoidMessage} from './createUnit'
 import {createStateRef, addRefOp} from './stateRef'
 import {mov, calc, read, userFnCall} from './step'
 import {processArgsToConfig} from './config'
@@ -19,9 +19,13 @@ export function combine(...args: any[]): Store<any> {
   let stores
   let config
   ;[args, config] = processArgsToConfig(args)
-  const rawHandler = args[args.length - 1]
+  // skipVoid support, to be removed in effector 24
+  const maybeExtConfig = args[args.length - 1]
+  const isExtendedConfig = !is.store(maybeExtConfig) && isObject(maybeExtConfig)
+  const extConfig = isExtendedConfig && maybeExtConfig
+  const rawHandler = isExtendedConfig ? args[args.length - 2] : maybeExtConfig
   if (isFunction(rawHandler)) {
-    stores = args.slice(0, -1)
+    stores = args.slice(0, isExtendedConfig ? -2 : -1)
     handler = rawHandler
   } else {
     stores = args
@@ -75,6 +79,7 @@ export function combine(...args: any[]): Store<any> {
     structStoreShape,
     config,
     handler,
+    extConfig,
   )
 }
 
@@ -84,6 +89,7 @@ const storeCombination = (
   obj: any,
   config?: Config,
   fn?: (upd: any) => any,
+  extConfig?: {skipVoid?: boolean},
 ) => {
   const clone = isArray ? (list: any) => [...list] : (obj: any) => ({...obj})
   const defaultState: Record<string, any> = isArray ? [] : {}
@@ -97,6 +103,7 @@ const storeCombination = (
   const store = createStore(stateNew, {
     name: unitObjectName(obj),
     derived: true,
+    ...extConfig,
     and: config,
   })
   const storeStateRef = getStoreState(store)
@@ -150,7 +157,7 @@ const storeCombination = (
     /**
      * `read` with `sampler` priority is used to prevent cases,
      *  where `combine` triggers are duplicated
-     * 
+     *
      *  basically, this makes `sample` and `combine` priorities equal
      */
     read(rawShape, true, true),
@@ -184,6 +191,11 @@ const storeCombination = (
   if (!readTemplate()) {
     if (fn) {
       const computedValue = fn(stateNew)
+
+      if (isVoid(computedValue) && (!extConfig || !("skipVoid" in extConfig))) {
+        console.error(requireExplicitSkipVoidMessage)
+      }
+
       storeStateRef.current = computedValue
       storeStateRef.initial = computedValue
       store.defaultState = computedValue
diff --git a/src/effector/createUnit.ts b/src/effector/createUnit.ts
index 3271ef86..01d271d4 100644
--- a/src/effector/createUnit.ts
+++ b/src/effector/createUnit.ts
@@ -224,6 +224,10 @@ function on<State>(
   })
   return store
 }
+
+export const requireExplicitSkipVoidMessage =
+  'undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option'
+
 export function createStore<State>(
   defaultState: State,
   props?: Config,
@@ -233,6 +237,14 @@ export function createStore<State>(
   const updates = createEvent({named: 'updates', derived: true})
   applyTemplate('storeBase', plainState)
   const plainStateId = plainState.id
+
+  // skipVoid deprecation rules
+  const explicitSkipVoid = 'skipVoid' in config
+  const voidValueAllowed = explicitSkipVoid && !config.skipVoid
+  const skipVoidTrueSet = explicitSkipVoid && config.skipVoid
+
+  deprecate(!skipVoidTrueSet, '{skipVoid: true}', 'updateFilter')
+
   const store = {
     subscribers: new Map(),
     updates,
@@ -283,30 +295,27 @@ export function createStore<State>(
       }
       return store
     },
-    map(fn: (value: any) => any, forbiddenArgument: any) {
-      assert(
-        isVoid(forbiddenArgument),
-        'second argument of store.map is not supported, use updateFilter instead',
-      )
-      let config
+    map(fn: (value: any) => any, outerConfig: Config) {
+      let mapConfig: Config | undefined
       if (isObject(fn)) {
-        config = fn
+        mapConfig = (fn as any)
         fn = (fn as unknown as {fn: (value: any) => any}).fn
       }
       let lastResult
       const storeState = store.getState()
+      const parentStateVoid = isVoid(storeState)
       const template = readTemplate()
       if (template) {
         lastResult = null
-      } else if (!isVoid(storeState)) {
+      } else if (!parentStateVoid || (parentStateVoid && voidValueAllowed)) {
         lastResult = fn(storeState)
       }
 
       const innerStore: Store<any> = createStore(lastResult, {
         name: `${store.shortName} → *`,
         derived: true,
-        // @ts-expect-error some mismatch in config types
-        and: config,
+        ...outerConfig,
+        and: mapConfig,
       })
       const linkNode = updateStore(store, innerStore, MAP, callStack, fn)
       addRefOp(getStoreState(innerStore), {
@@ -345,7 +354,18 @@ export function createStore<State>(
         return upd
       }),
       read(plainState),
-      calc((upd, _, {a, b}) => !isVoid(upd) && (upd !== a || b), true),
+      calc((upd, _, {a, b}) => {
+        const isVoidUpdate = isVoid(upd)
+
+        if (isVoidUpdate && !explicitSkipVoid) {
+          console.error(requireExplicitSkipVoidMessage)
+        }
+
+        return (
+          ((isVoidUpdate && voidValueAllowed) || !isVoidUpdate) &&
+          (upd !== a || b)
+        )
+      }, true),
       updateFilter && userFnCall(callStackAReg, true),
       mov({from: STACK, target: plainState}),
     ],
@@ -369,10 +389,15 @@ export function createStore<State>(
   if (!sid && !ignored && !derived) {
     setMeta(store, 'warnSerialize', true)
   }
+  const isVoidDefaultState = isVoid(defaultState)
+  const canVoid = (isVoidDefaultState && voidValueAllowed)
   assert(
-    derived || !isVoid(defaultState),
-    "current state can't be undefined, use null instead",
+    derived || !isVoidDefaultState || canVoid,
+    requireExplicitSkipVoidMessage,
   )
+  if (derived && (isVoidDefaultState && !explicitSkipVoid)) {
+    console.error(requireExplicitSkipVoidMessage)
+  }
   own(store, [updates])
   if (config?.domain) {
     config.domain.hooks.store(store)
diff --git a/src/effector/index.h.ts b/src/effector/index.h.ts
index 747bd98e..7cc39419 100644
--- a/src/effector/index.h.ts
+++ b/src/effector/index.h.ts
@@ -41,6 +41,7 @@ export type Config = {
   handler?: Function
   derived?: boolean
   serialize?: 'ignore'
+  skipVoid?: boolean
 }
 
 export type Node = {


Test patch:
diff --git a/beta/src/content/docs/en/api/effector/createStore.md b/beta/src/content/docs/en/api/effector/createStore.md
index b27f4a58..8a1ce99f 100644
--- a/beta/src/content/docs/en/api/effector/createStore.md
+++ b/beta/src/content/docs/en/api/effector/createStore.md
@@ -13,6 +13,7 @@ createStore<T>(defaultState: T): Store<T>
 createStore<T, SerializedState extends Json = Json>(defaultState: T, config: {
   name?: string
   updateFilter?: (update: T, current: T) => boolean
+  skipVoid?: boolean
   serialize?: 'ignore' | {
           write: (state: State) => SerializedState
           read: (json: SerializedState) => State
@@ -29,6 +30,7 @@ createStore<T, SerializedState extends Json = Json>(defaultState: T, config: {
    - `serialize: 'ignore'`: Option to disable store serialization when [serialize](/en/api/effector/serialize) is called _(since `effector 22.0.0`)_
    - `serialize` (_Object_): Configuration object to handle store state serialization in custom way. `write` – called on [serialize](/en/api/effector/serialize), transforms value to JSON value – primitive type or plain object/array. `read` – parse store state from JSON value, called on [fork](/en/api/effector/fork), if provided `values` is the result of `serialize` call.
    - `domain`: (_Domain_): Domain to attach store to after creation.
+   - `skipVoid`: (_boolean_): Flag to control, how specifically store should handle `undefined` value _(since `effector 23.0.0`)_. If set to `false` - store will use `undefined` as a value. If set to `true` (deprecated), store will read `undefined` as a "skip update" command and will do nothing.
 
 **Throws**
 
diff --git a/src/effector/__tests__/is.test.ts b/src/effector/__tests__/is.test.ts
index 12dc4082..ba66b4db 100644
--- a/src/effector/__tests__/is.test.ts
+++ b/src/effector/__tests__/is.test.ts
@@ -50,8 +50,8 @@ test('is.targetable', () => {
   const $store = createStore(0)
   const domain = createDomain()
   const scope = fork()
-  const mappedEvent = event.map(() => {})
-  const $mapped = $store.map(() => {})
+  const mappedEvent = event.map(() => null)
+  const $mapped = $store.map(() => null)
 
   expect(is.targetable(event)).toBe(true)
   expect(is.targetable(fx)).toBe(true)
diff --git a/src/effector/__tests__/store/index.test.ts b/src/effector/__tests__/store/index.test.ts
index 8153b0d1..ff167c8e 100644
--- a/src/effector/__tests__/store/index.test.ts
+++ b/src/effector/__tests__/store/index.test.ts
@@ -1,12 +1,15 @@
-import {createStore, createEvent, createEffect} from 'effector'
+import {
+  createStore,
+  createEvent,
+  createEffect,
+  sample,
+  combine,
+  fork,
+  allSettled,
+  serialize,
+} from 'effector'
 import {argumentHistory} from 'effector/fixtures'
 
-test('createStore throw on undefined', () => {
-  expect(() => createStore(undefined)).toThrowErrorMatchingInlineSnapshot(
-    `"current state can't be undefined, use null instead"`,
-  )
-})
-
 describe('.map', () => {
   it('supports basic mapping', () => {
     const fn = jest.fn()
@@ -54,16 +57,6 @@ describe('.map', () => {
     `)
   })
 
-  test('second argument throw an error', () => {
-    const $a = createStore(10)
-    expect(() => {
-      // @ts-expect-error
-      $a.map((x, y) => x + y, 2)
-    }).toThrowErrorMatchingInlineSnapshot(
-      `"second argument of store.map is not supported, use updateFilter instead"`,
-    )
-  })
-
   it('supports nested mapping with updates skipping', () => {
     const a = createStore(null)
     const f = jest.fn(a => {
@@ -320,3 +313,230 @@ describe('updateFilter', () => {
     `)
   })
 })
+
+describe('void skip pattern deprecation', () => {
+  let warn: jest.SpyInstance<void, [message?: any, ...optionalParams: any[]]>
+  beforeEach(() => {
+    warn = jest.spyOn(console, 'error').mockImplementation(() => {})
+  })
+  afterEach(() => {
+    warn.mockRestore()
+  })
+
+  function getWarning() {
+    return warn.mock.calls.map(([msg]) => msg).join('\n')
+  }
+
+  describe('writable stores', () => {
+    test('createStore throw on undefined', () => {
+      expect(() => createStore(undefined)).toThrowErrorMatchingInlineSnapshot(
+        `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+      )
+    })
+    test('createStore throw on undefined, if used with {skipVoid: true}', () => {
+      expect(() =>
+        createStore(undefined, {skipVoid: true}),
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+      )
+    })
+    test('createStore do not throw on undefined with skipVoid: false', () => {
+      expect(() => createStore(undefined, {skipVoid: false})).not.toThrow()
+    })
+    test('createStore warns deprecation, if used with {skipVoid: true}', () => {
+      expect(() => createStore(null, {skipVoid: true})).not.toThrow()
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+      )
+    })
+    test('createStore does not warn anything, if {skipVoid} and undefined are not presented', () => {
+      expect(() => createStore(null)).not.toThrow()
+      expect(getWarning()).toBe('')
+    })
+
+    describe('store.on', () => {
+      test('store.on reducer skips updates with undefined, but shows warning', () => {
+        const inc = createEvent()
+        const store = createStore(0).on(inc, () => {})
+        inc()
+        expect(getWarning()).toMatchInlineSnapshot(
+          `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+        )
+      })
+      test('store.on reducer skips updates with undefined and does not warn, if store has {skipVoid: true} (only warning of the store itself is shown)', () => {
+        const inc = createEvent()
+        const store = createStore(0, {skipVoid: true}).on(inc, () => {})
+        expect(getWarning()).toMatchInlineSnapshot(
+          `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+        )
+        inc()
+        expect(getWarning()).toMatchInlineSnapshot(
+          `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+        )
+      })
+      test('store.on reducer allows undefined as a value, if store has {skipVoid: false}', () => {
+        const inc = createEvent()
+        const store = createStore(0, {skipVoid: false}).on(inc, () => {})
+        inc()
+        expect(getWarning()).toBe('')
+        expect(store.getState()).toBe(undefined)
+      })
+    })
+
+    describe('sample target', () => {
+      test('sample target skips updates with undefined, but shows warning', () => {
+        const inc = createEvent()
+        const store = createStore<number | void>(0)
+        sample({clock: inc, fn: () => {}, target: store})
+        inc()
+        expect(getWarning()).toMatchInlineSnapshot(
+          `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+        )
+      })
+      test('sample target skips updates with undefined, but shows warning, if store has {skipVoid: true}', () => {
+        const inc = createEvent()
+        const store = createStore<number | void>(0, {skipVoid: true})
+        sample({clock: inc, fn: () => {}, target: store})
+        inc()
+        expect(getWarning()).toMatchInlineSnapshot(
+          `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+        )
+      })
+      test('sample target allows undefined as a value, if store has {skipVoid: false}', () => {
+        const inc = createEvent()
+        const store = createStore<number | void>(0, {skipVoid: false})
+        sample({clock: inc, fn: () => {}, target: store})
+        inc()
+        expect(getWarning()).toBe('')
+        expect(store.getState()).toBe(undefined)
+      })
+    })
+
+    describe('Fork API', () => {
+      test('undefined as a value is allowed in serialize result, if store has {skipVoid: false}', async () => {
+        const inc = createEvent()
+        const store = createStore(0, {skipVoid: false}).on(inc, () => {})
+
+        const scope = fork()
+
+        await allSettled(inc, {scope})
+
+        expect(scope.getState(store)).toBe(undefined)
+        expect(serialize(scope)).toMatchObject({
+          [store.sid!]: undefined,
+        })
+      })
+    })
+  })
+
+  describe('store.map', () => {
+    test('store.map warn on initial undefined', () => {
+      createStore(null).map(() => {})
+
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+      )
+    })
+    test('store.map warn on initial undefined, if used with {skipVoid: true}', () => {
+      createStore(null).map(() => {}, {skipVoid: true})
+
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+      )
+    })
+    test('store.map skips updates with undefined, but shows warning', () => {
+      const inc = createEvent<number>()
+      const store = createStore(0)
+        .on(inc, (_, v) => v)
+        .map(x => (x > 3 ? undefined : x))
+      inc(4)
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+      )
+    })
+    test('store.map skips updates with undefined, but shows one warning, if used with {skipVoid: true}', () => {
+      const inc = createEvent<number>()
+      const store = createStore(0)
+        .on(inc, (_, v) => v)
+        .map(x => (x > 3 ? undefined : x), {skipVoid: true})
+      inc(4)
+      inc(5)
+      inc(6)
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+      )
+    })
+    test('store.map do not warn on initial undefined, if used with {skipVoid: false}', () => {
+      expect(() => {
+        createStore(null).map(() => {}, {skipVoid: false})
+      }).not.toThrow()
+      expect(getWarning()).toBe('')
+    })
+    test('store.map do not warn on update undefined, if used with {skipVoid: false}', () => {
+      const inc = createEvent<number>()
+      const store = createStore(0)
+        .on(inc, (_, v) => v)
+        .map(x => (x > 3 ? undefined : x), {skipVoid: false})
+      inc(4)
+      expect(getWarning()).toBe('')
+      expect(store.getState()).toBe(undefined)
+    })
+  })
+
+  describe('combine', () => {
+    test('combine warns on initial undefined', () => {
+      combine({a: createStore(null)}, () => {})
+
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+      )
+    })
+    test('combine warn on initial undefined, if used with {skipVoid: true}', () => {
+      combine({a: createStore(null)}, () => {}, {skipVoid: true})
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+      )
+    })
+    test('combine skips updates with undefined, but shows warning', () => {
+      const inc = createEvent<number>()
+      const store = combine({a: createStore(0).on(inc, (_, v) => v)}, x =>
+        x.a > 3 ? undefined : x,
+      )
+      inc(4)
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option"`,
+      )
+    })
+    test('combine skips updates with undefined, but shows one warning, if used with {skipVoid: true}', () => {
+      const inc = createEvent<number>()
+      const store = combine(
+        {a: createStore(0).on(inc, (_, v) => v)},
+        x => (x.a > 3 ? undefined : x),
+        {skipVoid: true},
+      )
+      inc(4)
+      inc(5)
+      inc(6)
+      expect(getWarning()).toMatchInlineSnapshot(
+        `"{skipVoid: true} is deprecated, use updateFilter instead"`,
+      )
+    })
+    test('combine do not warn on initial undefined, if used with {skipVoid: false}', () => {
+      expect(() => {
+        combine({a: createStore(null)}, () => {}, {skipVoid: false})
+      }).not.toThrow()
+      expect(getWarning()).toBe('')
+    })
+    test('combine do not warn on update undefined, if used with {skipVoid: false}', () => {
+      const inc = createEvent<number>()
+      const store = combine(
+        {a: createStore(0).on(inc, (_, v) => v)},
+        x => (x.a > 3 ? undefined : x),
+        {skipVoid: false},
+      )
+      inc(4)
+      expect(getWarning()).toBe('')
+      expect(store.getState()).toBe(undefined)
+    })
+  })
+})
diff --git a/src/types/__tests__/effector/combine.test.ts b/src/types/__tests__/effector/combine.test.ts
index e1b3031a..ab06b489 100644
--- a/src/types/__tests__/effector/combine.test.ts
+++ b/src/types/__tests__/effector/combine.test.ts
@@ -355,7 +355,7 @@ test('possibly undefined store error message mismatch (should pass)', () => {
     hasNotActiveFunnels: $hasNotActiveFunnels,
     vacancyId: $vacancyField.map(v => {
       if (v) return v.id
-    }),
+    }, {skipVoid: false}),
   })
 
   const resultType: Store<{
diff --git a/src/types/__tests__/effector/guard.test.ts b/src/types/__tests__/effector/guard.test.ts
index a5ba86b2..c62d1c14 100644
--- a/src/types/__tests__/effector/guard.test.ts
+++ b/src/types/__tests__/effector/guard.test.ts
@@ -201,7 +201,7 @@ describe('guard(source, config)', () => {
             Type 'StoreWritable<string>' is not assignable to type 'Store<boolean> | ((source: number) => boolean)'.
               Type 'StoreWritable<string>' is not assignable to type 'Store<boolean>'.
                 Types of property 'map' are incompatible.
-                  Type '{ <T>(fn: (state: string, lastState?: T | undefined) => T): Store<T>; <T>(fn: (state: string, lastState: T) => T, firstState: T): Store<T>; }' is not assignable to type '{ <T>(fn: (state: boolean, lastState?: T | undefined) => T): Store<T>; <T>(fn: (state: boolean, lastState: T) => T, firstState: T): Store<T>; }'.
+                  Type '{ <T>(fn: (state: string, lastState?: T | undefined) => T, config?: { skipVoid?: boolean | undefined; } | undefined): Store<T>; <T>(fn: (state: string, lastState: T) => T, firstState: T): Store<...>; }' is not assignable to type '{ <T>(fn: (state: boolean, lastState?: T | undefined) => T, config?: { skipVoid?: boolean | undefined; } | undefined): Store<T>; <T>(fn: (state: boolean, lastState: T) => T, firstState: T): Store<...>; }'.
                     Types of parameters 'fn' and 'fn' are incompatible.
                       Types of parameters 'state' and 'state' are incompatible.
                         Type 'string' is not assignable to type 'boolean'.
diff --git a/src/types/__tests__/effector/index.test.ts b/src/types/__tests__/effector/index.test.ts
index 745fd7ec..a61652dd 100644
--- a/src/types/__tests__/effector/index.test.ts
+++ b/src/types/__tests__/effector/index.test.ts
@@ -381,7 +381,7 @@ describe('is guards', () => {
         "
         Type 'StoreWritable<number>' is not assignable to type 'Store<string>'.
           Types of property 'map' are incompatible.
-            Type '{ <T>(fn: (state: number, lastState?: T | undefined) => T): Store<T>; <T>(fn: (state: number, lastState: T) => T, firstState: T): Store<T>; }' is not assignable to type '{ <T>(fn: (state: string, lastState?: T | undefined) => T): Store<T>; <T>(fn: (state: string, lastState: T) => T, firstState: T): Store<T>; }'.
+            Type '{ <T>(fn: (state: number, lastState?: T | undefined) => T, config?: { skipVoid?: boolean | undefined; } | undefined): Store<T>; <T>(fn: (state: number, lastState: T) => T, firstState: T): Store<...>; }' is not assignable to type '{ <T>(fn: (state: string, lastState?: T | undefined) => T, config?: { skipVoid?: boolean | undefined; } | undefined): Store<T>; <T>(fn: (state: string, lastState: T) => T, firstState: T): Store<...>; }'.
               Types of parameters 'fn' and 'fn' are incompatible.
                 Types of parameters 'state' and 'state' are incompatible.
                   Type 'number' is not assignable to type 'string'.
diff --git a/src/types/__tests__/forest/index.test.ts b/src/types/__tests__/forest/index.test.ts
index fc4ad0bb..9dae77c9 100644
--- a/src/types/__tests__/forest/index.test.ts
+++ b/src/types/__tests__/forest/index.test.ts
@@ -37,7 +37,7 @@ describe('text', () => {
           Type 'Store<{ value: string; } | null>' is not assignable to type 'DOMProperty | AttributeStoreInput | (DOMProperty | AttributeStoreInput)[] | undefined'.
             Type 'Store<{ value: string; } | null>' is not assignable to type 'AttributeStoreInput'.
               Types of property 'map' are incompatible.
-                Type '{ <T>(fn: (state: { value: string; } | null, lastState?: T | undefined) => T): Store<T>; <T>(fn: (state: { value: string; } | null, lastState: T) => T, firstState: T): Store<...>; }' is not assignable to type '{ <T>(fn: (state: string | number | boolean | null, lastState?: T | undefined) => T): Store<T>; <T>(fn: (state: string | number | boolean | null, lastState: T) => T, firstState: T): Store<...>; }'.
+                Type '{ <T>(fn: (state: { value: string; } | null, lastState?: T | undefined) => T, config?: { skipVoid?: boolean | undefined; } | undefined): Store<T>; <T>(fn: (state: { value: string; } | null, lastState: T) => T, firstState: T): Store<...>; }' is not assignable to type '{ <T>(fn: (state: string | number | boolean | null, lastState?: T | undefined) => T, config?: { skipVoid?: boolean | undefined; } | undefined): Store<T>; <T>(fn: (state: string | number | boolean | null, lastState: T) => T, firstState: T): Store<...>; }'.
                   Types of parameters 'fn' and 'fn' are incompatible.
                     Types of parameters 'state' and 'state' are incompatible.
                       Type '{ value: string; } | null' is not assignable to type 'string | number | boolean | null'.