Repository: effector/effector
Pull request #415 (2020-11-05 17:25:02)

Add type tests for prepend, improve prepend type
`prepend` method infers payload type from the function argument if it exists... and if there is no argument the resulting payload type will be of type `unknown`. This is a bug because a user cannot call `Event<unknown>` without params -  it waits for 1 argument with type `unknown`.

```ts
const event = createEvent<string>();
const prepended = event.prepend(() => 'foo') // Event<unknown>
prepended() // TS error: Expected 1 arguments, but got 0.
```

To bypass the type error, a user needs to specify `void` manually: 
```ts
const event = createEvent<string>();
const prepended = event.prepend<void>(() => 'foo') // Event<void>
prepended() // ok
```

But we can fix this by adding a default type to prepend's `Before`:
```ts
  prepend<Before = void>(fn: (_: Before) => Payload): Event<Before>
```

As the result, TypeScript will show an error when you directly call `Event<void>` with an argument, which is good too:

```ts
const event = createEvent<string>()
const prepended = event.prepend(() => 'foo')
prepended(1) // Argument of type 'number' is not assignable to parameter of type 'void'.
```

Now it can be seen that the addition of the default value improves typing in both cases.

P.S. I also added type tests for `.prepend` and the described cases.


Patch:
diff --git a/packages/effector/index.d.ts b/packages/effector/index.d.ts
index 20bd0985..bdae0d96 100644
--- a/packages/effector/index.d.ts
+++ b/packages/effector/index.d.ts
@@ -116,7 +116,7 @@ export interface Event<Payload> extends Unit<Payload> {
   }): Event<T>
   filter(config: {fn(payload: Payload): boolean}): Event<Payload>
   filterMap<T>(fn: (payload: Payload) => T | undefined): Event<T>
-  prepend<Before>(fn: (_: Before) => Payload): Event<Before>
+  prepend<Before = void>(fn: (_: Before) => Payload): Event<Before>
   subscribe(observer: Observer<Payload>): Subscription
   thru<U>(fn: (event: Event<Payload>) => U): U
   getType(): string


Test patch:
diff --git a/src/types/__tests__/effector/event.test.js b/src/types/__tests__/effector/event.test.js
index 6ce264b0..c2e70acb 100644
--- a/src/types/__tests__/effector/event.test.js
+++ b/src/types/__tests__/effector/event.test.js
@@ -148,6 +148,137 @@ describe('#filterMap', () => {
     `)
   })
 })
+describe('#prepend', () => {
+  test('infer argument type', () => {
+    const event = createEvent<string>()
+    const prepended = event.prepend((arg: number) => 'foo')
+    prepended(1)
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      no errors
+
+      --flow--
+      no errors
+      "
+    `)
+  })
+  test('infer void type', () => {
+    const event = createEvent<string>()
+    const prepended = event.prepend<void>(() => 'foo')
+    prepended()
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      no errors
+
+      --flow--
+      no errors
+      "
+    `)
+  })
+  test('infer optional type', () => {
+    const event = createEvent<string>()
+    const prepended = event.prepend((arg?: number) => 'foo')
+    prepended()
+    prepended(1)
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      no errors
+
+      --flow--
+      no errors
+      "
+    `)
+  })
+  test('argument type mismatch', () => {
+    const event = createEvent<string>()
+    const prepended = event.prepend((arg: number) => 'foo')
+    prepended('')
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      No overload matches this call.
+        Overload 1 of 2, '(payload: number): number', gave the following error.
+          Argument of type 'string' is not assignable to parameter of type 'number'.
+        Overload 2 of 2, '(this: \\"Error: Expected 1 argument, but got 0\\", payload?: number | undefined): void', gave the following error.
+          The 'this' context of type 'void' is not assignable to method's 'this' of type '\\"Error: Expected 1 argument, but got 0\\"'.
+
+      --flow--
+      Cannot call 'prepended' with empty string bound to 'payload'
+        prepended('')
+                  ^^
+        string [1] is incompatible with number [2]. [incompatible-call]
+            prepended('')
+                  [1] ^^
+            const prepended = event.prepend((arg: number) => 'foo')
+                                              [2] ^^^^^^
+      "
+    `)
+  })
+  test('void type mismatch', () => {
+    const event = createEvent<string>()
+    const prepended = event.prepend(() => 'foo')
+    prepended('')
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      No overload matches this call.
+        Overload 1 of 2, '(payload: void): void', gave the following error.
+          Argument of type 'string' is not assignable to parameter of type 'void'.
+        Overload 2 of 2, '(this: void, payload?: void | undefined): void', gave the following error.
+          Argument of type '\\"\\"' is not assignable to parameter of type 'void | undefined'.
+
+      --flow--
+      no errors
+      "
+    `)
+  })
+  test('target event type mismatch', () => {
+    const event = createEvent<number>()
+    const prepended = event.prepend((arg: boolean) => 'foo')
+    prepended(true)
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      Argument of type '(arg: boolean) => string' is not assignable to parameter of type '(_: boolean) => number'.
+        Type 'string' is not assignable to type 'number'.
+
+      --flow--
+      Cannot call 'event.prepend' with function bound to 'fn'
+        const prepended = event.prepend((arg: boolean) => 'foo')
+                                                          ^^^^^
+        string [1] is incompatible with number [2] in the return value. [incompatible-call]
+            const prepended = event.prepend((arg: boolean) => 'foo')
+                                                          [1] ^^^^^
+            const event = createEvent<number>()
+                                  [2] ^^^^^^
+      "
+    `)
+  })
+  test('void target event edge case', () => {
+    const event: Event<void> = createEvent()
+    const prepended = event.prepend((arg: number) => 'foo') // returns string
+    prepended(1)
+    expect(typecheck).toMatchInlineSnapshot(`
+      "
+      --typescript--
+      no errors
+
+      --flow--
+      Cannot call 'event.prepend' with function bound to 'fn'
+        const prepended = event.prepend((arg: number) => 'foo') // returns string
+                                                         ^^^^^
+        string [1] is incompatible with undefined [2] in the return value. [incompatible-call]
+            const prepended = event.prepend((arg: number) => 'foo') // returns string
+                                                         [1] ^^^^^
+            const event: Event<void> = createEvent()
+                           [2] ^^^^
+      "
+    `)
+  })
+})
 test('void function interop (should pass)', () => {
   /*::
   type unknown = any;