Skip to content

Commit fb9764f

Browse files
authored
fix(types): accept MockInstance (again) (#31)
1 parent 6ddd8b2 commit fb9764f

File tree

7 files changed

+118
-84
lines changed

7 files changed

+118
-84
lines changed

src/behaviors.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { equals } from '@vitest/expect'
22

33
import type {
4-
AnyCallable,
54
AnyFunction,
5+
Mock,
66
ParametersOf,
77
ReturnTypeOf,
88
WithMatchers,
@@ -12,27 +12,25 @@ export interface WhenOptions {
1212
times?: number
1313
}
1414

15-
export interface BehaviorStack<TFunc extends AnyCallable> {
16-
use: (
17-
args: ParametersOf<TFunc>,
18-
) => BehaviorEntry<ParametersOf<TFunc>> | undefined
15+
export interface BehaviorStack<TParameters extends unknown[], TReturn> {
16+
use: (args: TParameters) => BehaviorEntry<TParameters> | undefined
1917

20-
getAll: () => readonly BehaviorEntry<ParametersOf<TFunc>>[]
18+
getAll: () => readonly BehaviorEntry<TParameters>[]
2119

22-
getUnmatchedCalls: () => readonly ParametersOf<TFunc>[]
20+
getUnmatchedCalls: () => readonly TParameters[]
2321

2422
bindArgs: (
25-
args: WithMatchers<ParametersOf<TFunc>>,
23+
args: WithMatchers<TParameters>,
2624
options: WhenOptions,
27-
) => BoundBehaviorStack<ReturnTypeOf<TFunc>>
25+
) => BoundBehaviorStack<TParameters, TReturn>
2826
}
2927

30-
export interface BoundBehaviorStack<TReturn> {
28+
export interface BoundBehaviorStack<TParameters extends unknown[], TReturn> {
3129
addReturn: (values: TReturn[]) => void
3230
addResolve: (values: Awaited<TReturn>[]) => void
3331
addThrow: (values: unknown[]) => void
3432
addReject: (values: unknown[]) => void
35-
addDo: (values: AnyFunction[]) => void
33+
addDo: (values: ((...args: TParameters) => TReturn)[]) => void
3634
}
3735

3836
export interface BehaviorEntry<TArgs extends unknown[]> {
@@ -62,11 +60,16 @@ export interface BehaviorOptions<TValue> {
6260
maxCallCount: number | undefined
6361
}
6462

63+
export type BehaviorStackOf<TMock extends Mock> = BehaviorStack<
64+
ParametersOf<TMock>,
65+
ReturnTypeOf<TMock>
66+
>
67+
6568
export const createBehaviorStack = <
66-
TFunc extends AnyCallable,
67-
>(): BehaviorStack<TFunc> => {
68-
const behaviors: BehaviorEntry<ParametersOf<TFunc>>[] = []
69-
const unmatchedCalls: ParametersOf<TFunc>[] = []
69+
TMock extends Mock,
70+
>(): BehaviorStackOf<TMock> => {
71+
const behaviors: BehaviorEntry<ParametersOf<TMock>>[] = []
72+
const unmatchedCalls: ParametersOf<TMock>[] = []
7073

7174
return {
7275
getAll: () => behaviors,

src/debug.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55

66
import { type Behavior, BehaviorType } from './behaviors.ts'
77
import { getBehaviorStack } from './stubs.ts'
8-
import type { AnyCallable, Mock } from './types.ts'
8+
import type { Mock } from './types.ts'
99

1010
export interface DebugResult {
1111
name: string
@@ -20,9 +20,7 @@ export interface Stubbing {
2020
calls: readonly unknown[][]
2121
}
2222

23-
export const getDebug = <TFunc extends AnyCallable>(
24-
mock: Mock<TFunc>,
25-
): DebugResult => {
23+
export const getDebug = (mock: Mock): DebugResult => {
2624
const name = mock.getMockName()
2725
const behaviors = getBehaviorStack(mock)
2826
const unmatchedCalls = behaviors?.getUnmatchedCalls() ?? mock.mock.calls

src/fallback-implementation.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import type { AnyCallable, AsFunction, Mock } from './types.ts'
1+
import type { AsFunction, Mock } from './types.ts'
22

33
/** Get the fallback implementation of a mock if no matching stub is found. */
4-
export const getFallbackImplementation = <TFunc extends AnyCallable>(
5-
mock: Mock<TFunc>,
6-
): AsFunction<TFunc> | undefined => {
4+
export const getFallbackImplementation = <TMock extends Mock>(
5+
mock: TMock,
6+
): AsFunction<TMock> | undefined => {
77
return (
8-
(mock.getMockImplementation() as AsFunction<TFunc> | undefined) ??
8+
(mock.getMockImplementation() as AsFunction<TMock> | undefined) ??
99
getTinyspyInternals(mock)?.getOriginal()
1010
)
1111
}
1212

1313
/** Internal state from Tinyspy, where a mock's default implementation is stored. */
14-
interface TinyspyInternals<TFunc extends AnyCallable> {
15-
getOriginal: () => AsFunction<TFunc> | undefined
14+
interface TinyspyInternals<TMock extends Mock> {
15+
getOriginal: () => AsFunction<TMock> | undefined
1616
}
1717

1818
/**
@@ -24,9 +24,9 @@ interface TinyspyInternals<TFunc extends AnyCallable> {
2424
* The implementation remains present in tinyspy internal state,
2525
* which is stored on a Symbol key in the mock object.
2626
*/
27-
const getTinyspyInternals = <TFunc extends AnyCallable>(
28-
mock: Mock<TFunc>,
29-
): TinyspyInternals<TFunc> | undefined => {
27+
const getTinyspyInternals = <TMock extends Mock>(
28+
mock: TMock,
29+
): TinyspyInternals<TMock> | undefined => {
3030
const maybeTinyspy = mock as unknown as Record<PropertyKey, unknown>
3131

3232
for (const key of Object.getOwnPropertySymbols(maybeTinyspy)) {
@@ -38,7 +38,7 @@ const getTinyspyInternals = <TFunc extends AnyCallable>(
3838
'getOriginal' in maybeTinyspyInternals &&
3939
typeof maybeTinyspyInternals.getOriginal === 'function'
4040
) {
41-
return maybeTinyspyInternals as TinyspyInternals<TFunc>
41+
return maybeTinyspyInternals as TinyspyInternals<TMock>
4242
}
4343
}
4444

src/stubs.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
import {
22
type BehaviorStack,
3+
type BehaviorStackOf,
34
BehaviorType,
45
createBehaviorStack,
56
} from './behaviors.ts'
67
import { NotAMockFunctionError } from './errors.ts'
78
import { getFallbackImplementation } from './fallback-implementation.ts'
8-
import type { AnyCallable, Mock, ParametersOf } from './types.ts'
9+
import type {
10+
AsFunction,
11+
Mock,
12+
MockSource,
13+
ParametersOf,
14+
ReturnTypeOf,
15+
} from './types.ts'
916

1017
const BEHAVIORS_KEY = Symbol.for('vitest-when:behaviors')
1118

12-
interface WhenStubImplementation<TFunc extends AnyCallable> {
13-
(...args: ParametersOf<TFunc>): unknown
14-
[BEHAVIORS_KEY]: BehaviorStack<TFunc>
19+
interface WhenStubImplementation<TMock extends Mock> {
20+
(...args: ParametersOf<TMock>): unknown
21+
[BEHAVIORS_KEY]: BehaviorStack<ParametersOf<TMock>, ReturnTypeOf<TMock>>
1522
}
1623

17-
export const configureMock = <TFunc extends AnyCallable>(
18-
mock: Mock<TFunc>,
19-
): BehaviorStack<TFunc> => {
24+
export const configureMock = <TMock extends Mock>(
25+
mock: TMock,
26+
): BehaviorStackOf<TMock> => {
2027
const existingBehaviorStack = getBehaviorStack(mock)
2128

2229
if (existingBehaviorStack) {
2330
return existingBehaviorStack
2431
}
2532

26-
const behaviorStack = createBehaviorStack<TFunc>()
33+
const behaviorStack = createBehaviorStack<TMock>()
2734
const fallbackImplementation = getFallbackImplementation(mock)
2835

29-
const implementation = (...args: ParametersOf<TFunc>) => {
36+
const implementation = (...args: ParametersOf<TMock>) => {
3037
const behavior = behaviorStack.use(args)?.behavior ?? {
3138
type: BehaviorType.DO,
3239
callback: fallbackImplementation,
@@ -51,6 +58,7 @@ export const configureMock = <TFunc extends AnyCallable>(
5158
}
5259

5360
case BehaviorType.DO: {
61+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
5462
return behavior.callback?.(...args)
5563
}
5664
}
@@ -63,26 +71,26 @@ export const configureMock = <TFunc extends AnyCallable>(
6371
return behaviorStack
6472
}
6573

66-
export const validateMock = <TFunc extends AnyCallable>(
67-
maybeMock: TFunc | Mock<TFunc>,
68-
): Mock<TFunc> => {
74+
export const validateMock = <TSource extends MockSource>(
75+
maybeMock: TSource,
76+
): Mock<TSource> => {
6977
if (
7078
typeof maybeMock === 'function' &&
7179
'mockImplementation' in maybeMock &&
7280
typeof maybeMock.mockImplementation === 'function'
7381
) {
74-
return maybeMock
82+
return maybeMock as Mock<TSource>
7583
}
7684

7785
throw new NotAMockFunctionError(maybeMock)
7886
}
7987

80-
export const getBehaviorStack = <TFunc extends AnyCallable>(
81-
mock: Mock<TFunc>,
82-
): BehaviorStack<TFunc> | undefined => {
88+
export const getBehaviorStack = <TMock extends Mock>(
89+
mock: TMock,
90+
): BehaviorStackOf<TMock> | undefined => {
8391
const existingImplementation = mock.getMockImplementation() as
84-
| WhenStubImplementation<TFunc>
85-
| TFunc
92+
| WhenStubImplementation<TMock>
93+
| AsFunction<TMock>
8694
| undefined
8795

8896
return existingImplementation && BEHAVIORS_KEY in existingImplementation

src/types.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,71 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
/** Common type definitions. */
23
import type { AsymmetricMatcher } from '@vitest/expect'
34
import type { MockedClass, MockedFunction } from 'vitest'
45

56
/** Any function. */
6-
export type AnyFunction = (...args: never[]) => unknown
7+
export type AnyFunction = (...args: any[]) => any
78

89
/** Any constructor. */
9-
export type AnyConstructor = new (...args: never[]) => unknown
10+
export type AnyConstructor = new (...args: any[]) => any
1011

11-
/** Any callable, for use in `extends` */
12-
export type AnyCallable = AnyFunction | AnyConstructor
12+
/**
13+
* Minimally typed version of Vitest's `MockInstance`.
14+
*
15+
* Ensures backwards compatibility with vitest@<=1
16+
*/
17+
export interface MockInstance<TFunc extends AnyFunction = AnyFunction> {
18+
getMockName(): string
19+
getMockImplementation(): TFunc | undefined
20+
mockImplementation: (impl: TFunc) => this
21+
mock: MockContext<TFunc>
22+
}
23+
24+
export interface MockContext<TFunc extends AnyFunction> {
25+
calls: Parameters<TFunc>[]
26+
}
27+
28+
/** A function, constructor, or `vi.spyOn` return that's been mocked. */
29+
export type MockSource = AnyFunction | AnyConstructor | MockInstance
1330

1431
/** Extract parameters from either a function or constructor. */
15-
export type ParametersOf<TFunc extends AnyCallable> = TFunc extends new (
32+
export type ParametersOf<TMock extends MockSource> = TMock extends new (
1633
...args: infer P
1734
) => unknown
1835
? P
19-
: TFunc extends (...args: infer P) => unknown
36+
: TMock extends (...args: infer P) => unknown
2037
? P
21-
: never
38+
: TMock extends MockInstance<(...args: infer P) => unknown>
39+
? P
40+
: never
2241

2342
/** Extract return type from either a function or constructor */
24-
export type ReturnTypeOf<TFunc extends AnyCallable> = TFunc extends new (
43+
export type ReturnTypeOf<TMock extends MockSource> = TMock extends new (
2544
...args: never[]
2645
) => infer R
2746
? R
28-
: TFunc extends (...args: never[]) => infer R
47+
: TMock extends (...args: any[]) => infer R
2948
? R
30-
: never
49+
: TMock extends MockInstance<(...args: never[]) => infer R>
50+
? R
51+
: never
3152

32-
export type AsFunction<TFunc extends AnyCallable> = (
33-
...args: ParametersOf<TFunc>
34-
) => ReturnTypeOf<TFunc>
53+
/** Convert a function or constructor type into a function type. */
54+
export type AsFunction<TMock extends MockSource> = (
55+
...args: ParametersOf<TMock>
56+
) => ReturnTypeOf<TMock>
3557

3658
/** Accept a value or an AsymmetricMatcher in an arguments array */
3759
export type WithMatchers<T extends unknown[]> = {
3860
[K in keyof T]: AsymmetricMatcher<unknown> | T[K]
3961
}
4062

41-
export type Mock<TFunc extends AnyCallable> = TFunc extends AnyFunction
42-
? MockedFunction<TFunc>
43-
: TFunc extends AnyConstructor
44-
? MockedClass<TFunc>
45-
: never
63+
/** A mocked function or constructor. */
64+
export type Mock<TMock extends MockSource = MockSource> =
65+
TMock extends MockInstance<infer TFunc>
66+
? MockedFunction<TFunc>
67+
: TMock extends AnyFunction
68+
? MockedFunction<TMock>
69+
: TMock extends AnyConstructor
70+
? MockedClass<TMock>
71+
: never

src/vitest-when.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { WhenOptions } from './behaviors.ts'
22
import { type DebugResult, getDebug } from './debug.ts'
33
import { configureMock, validateMock } from './stubs.ts'
44
import type {
5-
AnyCallable,
65
AsFunction,
76
Mock,
7+
MockSource,
88
ParametersOf,
99
ReturnTypeOf,
1010
WithMatchers,
@@ -14,24 +14,24 @@ export { type Behavior, BehaviorType, type WhenOptions } from './behaviors.ts'
1414
export type { DebugResult, Stubbing } from './debug.ts'
1515
export * from './errors.ts'
1616

17-
export interface StubWrapper<TFunc extends AnyCallable> {
18-
calledWith<TArgs extends ParametersOf<TFunc>>(
17+
export interface StubWrapper<TMock extends Mock> {
18+
calledWith<TArgs extends ParametersOf<TMock>>(
1919
...args: WithMatchers<TArgs>
20-
): Stub<TFunc>
20+
): Stub<TMock>
2121
}
2222

23-
export interface Stub<TFunc extends AnyCallable> {
24-
thenReturn: (...values: ReturnTypeOf<TFunc>[]) => Mock<TFunc>
25-
thenResolve: (...values: Awaited<ReturnTypeOf<TFunc>>[]) => Mock<TFunc>
26-
thenThrow: (...errors: unknown[]) => Mock<TFunc>
27-
thenReject: (...errors: unknown[]) => Mock<TFunc>
28-
thenDo: (...callbacks: AsFunction<TFunc>[]) => Mock<TFunc>
23+
export interface Stub<TMock extends Mock> {
24+
thenReturn: (...values: ReturnTypeOf<TMock>[]) => TMock
25+
thenResolve: (...values: Awaited<ReturnTypeOf<TMock>>[]) => TMock
26+
thenThrow: (...errors: unknown[]) => TMock
27+
thenReject: (...errors: unknown[]) => TMock
28+
thenDo: (...callbacks: AsFunction<TMock>[]) => TMock
2929
}
3030

31-
export const when = <TFunc extends AnyCallable>(
32-
mock: TFunc | Mock<TFunc>,
31+
export const when = <TMockSource extends MockSource>(
32+
mock: TMockSource,
3333
options: WhenOptions = {},
34-
): StubWrapper<TFunc> => {
34+
): StubWrapper<Mock<TMockSource>> => {
3535
const validatedMock = validateMock(mock)
3636
const behaviorStack = configureMock(validatedMock)
3737

@@ -69,8 +69,8 @@ export interface DebugOptions {
6969
log?: boolean
7070
}
7171

72-
export const debug = <TFunc extends AnyCallable>(
73-
mock: TFunc | Mock<TFunc>,
72+
export const debug = (
73+
mock: MockSource,
7474
options: DebugOptions = {},
7575
): DebugResult => {
7676
const log = options.log ?? true

test/typing.test-d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,9 @@ describe('vitest-when type signatures', () => {
6969
})
7070

7171
it('should handle an spied function', () => {
72-
const target = { simple }
73-
vi.spyOn(target, 'simple')
72+
const target = vi.spyOn({ simple }, 'simple')
7473

75-
const result = subject.when(target.simple).calledWith(1).thenReturn('hello')
74+
const result = subject.when(target).calledWith(1).thenReturn('hello')
7675

7776
expectTypeOf(result.mock.calls).toEqualTypeOf<[number][]>()
7877
expectTypeOf(result).parameters.toEqualTypeOf<[number]>()

0 commit comments

Comments
 (0)