Repository: effector/effector (ts)

Issue:
Invalid types for `createEffect` and `attach` in factories with generics
Invoking `attach` within factories with an unbounded type parameter causes an error when the `effect` in `attach` returns that type parameter.

![image](https://github.com/effector/effector/assets/29187880/71cce103-586d-42f6-9e07-e9f2d415e208)

https://github.com/effector/effector/blob/879134505b41e83896352c9930c326f9a1cf39aa/packages/effector/index.d.ts#L2813-L2815

Reproduction (with error and proposed fix): https://tsplay.dev/NdbxdW
The `Awaited` type I used is [available in TS](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements) since version 4.5


Pull request:
Fix types for createEffect and attach in factories with generics
Fix #1069 

Patch:
diff --git a/packages/effector/index.d.ts b/packages/effector/index.d.ts
index 12916628..298a103e 100644
--- a/packages/effector/index.d.ts
+++ b/packages/effector/index.d.ts
@@ -58,7 +58,21 @@ export type EffectError<FX extends Effect<any, any, any>> = FX extends Effect<
 >
   ? E
   : never
-type AsyncResult<Done> = Done extends Promise<infer Async> ? Async : Done
+
+// Taken from the source code of typescript 4.5. Remove when we separate types for different versions
+/**
+ * Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
+ */
+type Awaited<T> = T extends null | undefined
+  ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
+  : T extends object // `await` only unwraps object types with a callable then. Non-object types are not unwrapped.
+  ? T extends {then(onfulfilled: infer F): any} // thenable, extracts the first argument to `then()`
+    ? F extends (value: infer V) => any // if the argument to `then` is callable, extracts the argument
+      ? Awaited<V> // recursively unwrap the value
+      : never // the argument to `then` was not callable.
+    : T // argument was not an object
+  : T // non-thenable
+
 type OptionalParams<Args extends any[]> =
   Args['length'] extends 0 // does handler accept 0 arguments?
     ? void // works since TS v3.3.3
@@ -79,7 +93,7 @@ type OptionalParams<Args extends any[]> =
     ? Args[0] | void
     : Args[0]
 type EffectByHandler<FN extends Function, Fail> = FN extends (...args: infer Args) => infer Done
-  ? Effect<OptionalParams<Args>, AsyncResult<Done>, Fail>
+  ? Effect<OptionalParams<Args>, Awaited<Done>, Fail>
   : never
 
 export const version: string
@@ -2811,7 +2825,7 @@ export function attach<
   domain?: Domain
   name?: string
 }): FX extends (source: any, ...args: infer Args) => infer Done
-  ? Effect<OptionalParams<Args>, AsyncResult<Done>>
+  ? Effect<OptionalParams<Args>, Awaited<Done>>
   : never
 /**
  * Creates independent instance of given effect. Used to add subscribers to effect call in a particular business case


Test patch:
diff --git a/src/types/__tests__/effector/attach.test.ts b/src/types/__tests__/effector/attach.test.ts
index eadaec76..2b1833be 100644
--- a/src/types/__tests__/effector/attach.test.ts
+++ b/src/types/__tests__/effector/attach.test.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-unused-vars */
-import {createStore, createEffect, attach, Effect} from 'effector'
+import {createStore, createEffect, attach, sample, Effect} from 'effector'
 
 const typecheck = '{global}'
 
@@ -34,6 +34,30 @@ describe('explicit generics', () => {
   })
 })
 
+test('factories with generics support', () => {
+  function createModel<T>() {
+    const $data = createStore<T | null>(null)
+
+    const loadFx = attach({
+      source: {
+        /* Assume we use some $api with external API we don't control */
+      },
+      effect() {
+        const result: any = /* API returns `any` */ null
+
+        return result as T /* explicit cast, becase we know result is T */
+      },
+    })
+
+    sample({clock: loadFx.doneData, target: $data})
+  }
+  expect(typecheck).toMatchInlineSnapshot(`
+    "
+    no errors
+    "
+  `)
+})
+
 describe('with source', () => {
   test('with single store (should pass)', () => {
     const foo = createStore<string>('foo')
diff --git a/src/types/__tests__/effector/effect.test.ts b/src/types/__tests__/effector/effect.test.ts
index fffa1dce..f26bba36 100644
--- a/src/types/__tests__/effector/effect.test.ts
+++ b/src/types/__tests__/effector/effect.test.ts
@@ -1,14 +1,29 @@
 /* eslint-disable no-unused-vars */
 import {
   createEffect,
+  createStore,
+  sample,
   Effect,
   Event,
-  /*::type*/ CompositeName,
-  /*::type*/ kind,
+  CompositeName,
+  kind,
 } from 'effector'
 
 const typecheck = '{global}'
 
+test('generics support', () => {
+  function createModel<T>() {
+    const $data = createStore<T | null>(null)
+    const fx = createEffect(() => null as T)
+    sample({clock: fx.doneData, target: $data})
+  }
+  expect(typecheck).toMatchInlineSnapshot(`
+    "
+    no errors
+    "
+  `)
+})
+
 test('createEffect', () => {
   const createEffect_effect1: Effect<number, string> = createEffect()
   const createEffect_effect2 = createEffect('', {