From af02f41bb88c1e16fa4a9499076030911c537540 Mon Sep 17 00:00:00 2001 From: jihyeon Date: Wed, 21 Jan 2026 00:22:56 +0900 Subject: [PATCH 1/2] feat(spy): support deep partial in vi.mocked (#8152) --- packages/spy/src/types.ts | 33 ++++++++++++++++++++++++- packages/vitest/src/integrations/vi.ts | 2 +- test/core/test/vi.spec.ts | 34 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index c228c85efaa1..fad6e7ae031d 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 a8db9df2acd5..f1c4322fef24 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 37602f31ddea..0ef0ab55085d 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', () => { From d5bb5f6c409d8569b47f99bce2bb0370f590d8db Mon Sep 17 00:00:00 2001 From: jihyeon Date: Wed, 21 Jan 2026 01:11:13 +0900 Subject: [PATCH 2/2] docs(vi): add deep partial example for vi.mocked (#8152) --- docs/api/vi.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/api/vi.md b/docs/api/vi.md index aa00a27940aa..8f45c50d1c47 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -262,7 +262,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 { @@ -272,6 +272,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] @@ -289,6 +293,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