Skip to content

Commit

Permalink
Merge pull request #1071 from effector/fix-attach-generic-factories-s…
Browse files Browse the repository at this point in the history
…upport

Fix types for createEffect and attach in factories with generics
  • Loading branch information
zerobias committed May 23, 2024
2 parents dc16813 + bab150e commit 89e7942
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 6 deletions.
20 changes: 17 additions & 3 deletions packages/effector/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion src/types/__tests__/effector/attach.test.ts
Original file line number Diff line number Diff line change
@@ -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}'

Expand Down Expand Up @@ -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')
Expand Down
19 changes: 17 additions & 2 deletions src/types/__tests__/effector/effect.test.ts
Original file line number Diff line number Diff line change
@@ -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('', {
Expand Down

0 comments on commit 89e7942

Please sign in to comment.