Skip to content

noctcore-code-quality/typed-mock-over-double-cast

An object of mocks must be typed against the real interface, not double-cast past the checker.

Package
code-qualityv0.4.2
Recommended preset
not listedopt-in
Autofix
No
Suggestions
No
Options
Optional
Type information
Not needed

{ get: jest.fn() } as unknown as ConfigService tells TypeScript to stop checking. When the real service renames get to getOrThrow, the test still mocks get, the code under test calls getOrThrow on an object that does not have it, and the failure (if the test fails at all) is a TypeError far from the cause. Worse, a mock that returns the wrong shape keeps compiling forever.

Type the double against the members the test uses. jest.Mocked<Pick<T, 'a' | 'b'>> (or Vitest’s Mocked<Pick<...>>) keeps every mocked member checked against T, and satisfies Partial<T> checks the literal without widening it.

Incorrect · src/metrics/basic-auth.guard.spec.ts
const config = {
get: jest.fn((key: string) => (key === 'METRICS_BASIC_AUTH' ? basicAuth : undefined)),
} as unknown as ConfigService;
Correct · src/metrics/basic-auth.guard.spec.ts
const config: jest.Mocked<Pick<ConfigService, 'get'>> = {
get: jest.fn((key: string) => (key === 'METRICS_BASIC_AUTH' ? basicAuth : undefined)),
};

An object literal cast as unknown as T (or as any as T, as never as T, and the angle-bracket forms <T><unknown>{ ... } and <T>({ ... } as unknown)) that contains a mock-function call (jest.fn() / vi.fn() by default) anywhere inside it: as a property, nested in another object, at the start of a chain (jest.fn().mockResolvedValue(...)) or inside a function property.

Incorrect · src/common/rate-limit.spec.ts · 2 reports
const opts = { ctx: { res: { setHeader: jest.fn() } } } as unknown as OnErrorOptions;
const host = {
switchToHttp: () => ({ getResponse: () => ({ status: jest.fn() }) }),
} as unknown as ArgumentsHost;
Correct · src/common/rate-limit.spec.ts
const opts = { ctx: { res: { setHeader: jest.fn() } } } satisfies DeepPartial<OnErrorOptions>;
const host: jest.Mocked<Pick<ArgumentsHost, 'switchToHttp'>> = {
switchToHttp: jest.fn(() => ({ getResponse: () => ({ status: jest.fn() }) })),
};

A mock created first and placed in the object by name counts too, shorthand ({ send }) or not ({ run: send }): the name must be bound to a mock-function call, or to a chain that starts with one, by its initialiser or by a plain assignment (let get; beforeEach(() => { get = jest.fn(); })).

Incorrect · src/mail/mailer.spec.ts · 2 reports
const send = vi.fn();
const mailer = { send } as unknown as Mailer;
let get: jest.Mock;
beforeEach(() => {
get = jest.fn().mockReturnValue('smtp://localhost');
config = { get } as unknown as ConfigService;
});
Correct · src/mail/mailer.spec.ts
const send = vi.fn();
const mailer: Mocked<Pick<Mailer, 'send'>> = { send };
let get: jest.Mock;
beforeEach(() => {
get = jest.fn().mockReturnValue('smtp://localhost');
config = { get } satisfies Partial<ConfigService>;
});
  • A single cast (as jest.Mocked<Pick<T, 'get'>>): TypeScript still checks that the two types overlap.
  • A double cast of an object with no mock in it (a data fixture), or of something that is not an object literal (existingDouble as unknown as T).
  • as unknown on its own.
  • A mock the rule cannot trace to its factory call in the same file: one that arrives through a spread ({ ...mocks }), an import, a parameter, a destructuring or a helper’s return value ({ send: makeSend() }).
  • A target matching allowTargets.
Option Type Default Meaning
mockFactories string[] ["jest.fn", "vi.fn"] Callees that create a mock function, as name or obj.name.
allowTargets string[] (globs) [] Cast targets that may be double-cast, matched against the type name (PrismaService, Prisma.TransactionClient) and the full type text (Parameters<Guard['canActivate']>[0]). For types too wide to Pick from.
'noctcore-code-quality/typed-mock-over-double-cast': ['error', {
// An ORM client and its transaction are too wide to Pick from.
allowTargets: ['PrismaService', 'Prisma.TransactionClient'],
}]
Correct · src/metrics/basic-auth.guard.spec.ts
// rule options: {"allowTargets":["PrismaService"]}
const prisma = { user: { findMany: jest.fn() } } as unknown as PrismaService;

In a codebase without strict types in its tests, where a double cast is the norm and a typed double would need a helper type library first.