From 71cb51ffc4434492ca297ee2a2636fb52925ca52 Mon Sep 17 00:00:00 2001 From: Dahm <60846068+j2h30728@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:22:44 +0900 Subject: [PATCH] fix(spy): support deep partial in vi.mocked (#8152) (#9493) --- docs/api/vi.md | 13 +++++++++- packages/spy/src/types.ts | 33 ++++++++++++++++++++++++- packages/vitest/src/integrations/vi.ts | 2 +- test/core/test/vi.spec.ts | 34 ++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/docs/api/vi.md b/docs/api/vi.md index 56e09143d..e72dcea06 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -264,7 +264,7 @@ function mocked( Type helper for TypeScript. Just returns the object that was passed. -When `partial` is `true` it will expect a `Partial` as a return value. By default, this will only make TypeScript believe that the first level values are mocked. You can pass down `{ deep: true }` as a second argument to tell TypeScript that the whole object is mocked, if it actually is. +When `partial` is `true` it will expect a `Partial` as a return value. By default, this will only make TypeScript believe that the first level values are mocked. You can pass down `{ deep: true }` as a second argument to tell TypeScript that the whole object is mocked, if it actually is. You can pass down `{ partial: true, deep: true }` to make nested objects also partial recursively. ```ts [example.ts] export function add(x: number, y: number): number { @@ -274,6 +274,10 @@ export function add(x: number, y: number): number { export function fetchSomething(): Promise { return fetch('https://vitest.dev/') } + +export function getUser(): { name: string; address: { city: string; zip: string } } { + return { name: 'John', address: { city: 'New York', zip: '10001' } } +} ``` ```ts [example.test.ts] @@ -291,6 +295,13 @@ test('mock return value with only partially correct typing', async () => { vi.mocked(example.fetchSomething, { partial: true }).mockResolvedValue({ ok: false }) // vi.mocked(example.someFn).mockResolvedValue({ ok: false }) // this is a type error }) + +test('mock return value with deep partial typing', async () => { + vi.mocked(example.getUser, { partial: true, deep: true }).mockReturnValue({ + address: { city: 'Los Angeles' }, + }) + expect(example.getUser().address.city).toBe('Los Angeles') +}) ``` ### vi.importActual diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index c228c85ef..fad6e7ae0 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -406,6 +406,37 @@ export type PartialMock = Mock< > > +type DeepPartial = T extends Procedure + ? T + : T extends Array + ? Array> + : T extends object + ? { [K in keyof T]?: DeepPartial } + : T + +type DeepPartialMaybePromise = T extends Promise> + ? Promise>> + : DeepPartial + +type DeepPartialResultFunction = T extends Constructable + ? ({ + new (...args: ConstructorParameters): InstanceType + }) + | ({ + (this: InstanceType, ...args: ConstructorParameters): void + }) + : T extends Procedure + ? (...args: Parameters) => DeepPartialMaybePromise> + : T + +type DeepPartialMock = Mock< + DeepPartialResultFunction< + T extends Mock + ? NonNullable> + : T + > +> + export type MaybeMockedConstructor = T extends Constructable ? Mock : T @@ -417,7 +448,7 @@ export type PartiallyMockedFunction = Parti } export type MockedFunctionDeep = Mock & MockedObjectDeep -export type PartiallyMockedFunctionDeep = PartialMock +export type PartiallyMockedFunctionDeep = DeepPartialMock & MockedObjectDeep export type MockedObject = MaybeMockedConstructor & { [K in Methods]: T[K] extends Procedure ? MockedFunction : T[K]; diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index a8db9df2a..f1c4322fe 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -320,7 +320,7 @@ export interface VitestUtils { * Type helper for TypeScript. Just returns the object that was passed. * * When `partial` is `true` it will expect a `Partial` as a return value. By default, this will only make TypeScript believe that - * the first level values are mocked. You can pass down `{ deep: true }` as a second argument to tell TypeScript that the whole object is mocked, if it actually is. + * the first level values are mocked. You can pass down `{ partial: true, deep: true }` to make nested objects also partial recursively. * @example * ```ts * import example from './example.js' diff --git a/test/core/test/vi.spec.ts b/test/core/test/vi.spec.ts index 37602f31d..0ef0ab550 100644 --- a/test/core/test/vi.spec.ts +++ b/test/core/test/vi.spec.ts @@ -108,6 +108,40 @@ describe('testing vi utils', () => { vi.mocked(fetchSomething).mockResolvedValue(new Response(null)) vi.mocked(fetchSomething, { partial: true }).mockResolvedValue({ ok: false }) } + + // #8152 + if (0) { + interface NestedObject { + level1: { + level2: { + value: string + count: number + } + name: string + } + items: string[] + } + + const mockNestedFactory = vi.fn<() => NestedObject>() + + vi.mocked(mockNestedFactory, { partial: true, deep: true }).mockReturnValue({ + level1: { level2: {} }, + }) + vi.mocked(mockNestedFactory, { partial: true, deep: true }).mockReturnValue({ + level1: {}, + }) + vi.mocked(mockNestedFactory, { partial: true, deep: true }).mockReturnValue({}) + vi.mocked(mockNestedFactory, { partial: true, deep: true }).mockReturnValue({ + items: ['a', 'b'], + }) + + const mockNestedAsyncFactory = vi.fn<() => Promise>() + + vi.mocked(mockNestedAsyncFactory, { partial: true, deep: true }).mockResolvedValue({ + level1: { level2: {} }, + }) + vi.mocked(mockNestedAsyncFactory, { partial: true, deep: true }).mockResolvedValue({}) + } }) test('vi.mocked with classes', () => { -- 2.51.2