PRACTICAL GUIDE / TypeScript typed mocks
TypeScript Typed Mocks Without Unsafe Casts
Build TypeScript typed mocks with jest.mocked, vi.mocked, satisfies, and fixture factories so signature or return-shape drift fails during compilation.
In this guide11 sections
- What Makes a TypeScript Mock Truly Typed?
- How Do Unsafe Casts Hide Mock Regressions?
- jest.mocked, vi.mocked, and Typed fn: Which Tool Should You Use?
- A Numbered Workflow for Replacing Unsafe Test Casts
- Code Example: Replace as jest.Mock with jest.mocked
- How Should You Build Complete Fixtures with Partial Overrides?
- Code Example: Keep a Vitest Dependency Mock Signature Intact
- Typed Full Mocks vs Partial Dependencies vs Fixture Factories
- Which Casts Are Acceptable in TypeScript Tests?
- Frequently Asked Questions About TypeScript Typed Mocks
- What is a typed mock in TypeScript?
- When should I use jest.mocked instead of jest.Mock?
- What is the Vitest equivalent of jest.mocked?
- Why is as unknown as unsafe in test fixtures?
- Should a mock factory return Partial of the production type?
- Do typed mocks remove the need for runtime tests?
- Next Steps: Make Mock Drift a Compile Failure
What you will learn
- What Makes a TypeScript Mock Truly Typed?
- How Do Unsafe Casts Hide Mock Regressions?
- jest.mocked, vi.mocked, and Typed fn: Which Tool Should You Use?
- A Numbered Workflow for Replacing Unsafe Test Casts
TypeScript typed mocks preserve the production function signature through jest.mocked(), vi.mocked(), jest.fn<typeof dependency>(), or vi.fn<typeof dependency>(). Valid fixtures use satisfies, complete factories, and narrow Partial<T> override inputs instead of force-casting incomplete values. The goal is simple: when a parameter, promise result, or required field changes, compilation should fail at the stale test setup.
That compile-time failure is only one layer of evidence. A typed double can still model unrealistic behavior, return the wrong business scenario, or omit an important runtime assertion. Good tests combine signature safety, valid fixture construction, controlled dependency behavior, and assertions against the subject's observable result.
What Makes a TypeScript Mock Truly Typed?
A mock is truly typed when it keeps the contract of the dependency it replaces. Parameter count and types matter. The return type matters. For asynchronous code, the resolved value matters. For generators, the yielded and returned values matter. Object fixtures must satisfy required fields. The runner then adds call tracking and behavior configuration without replacing those contracts with any.
Consider a production function:
fetchUser(id: string): Promise<User>
A typed mock rejects fetchUserMock(42), rejects mockResolvedValue("Ada"), and rejects a result object missing a required displayName. A broad mock that accepts any arguments and any result gives the test runner flexibility at the cost of the compiler's most useful warnings.
There are four related but distinct test values:
- A mocked imported function, where
jest.mocked(fetchUser)orvi.mocked(fetchUser)exposes runner methods on the real signature. - A new function double, where a generic
fnor typed implementation defines the dependency contract. - A fixture object, where
satisfies Useror a factory proves complete shape. - A partial dependency interface, where the subject intentionally consumes a smaller contract than the full production service.
Do not collapse these into one casting pattern. Function typing and fixture typing solve different problems. A typed function can still return a force-cast bad object. A valid object can still be configured on a mock whose parameters have become unchecked.
Overloads and generics need deliberate handling. A helper inferred from one implementation may retain only the chosen callable form rather than every production overload. If callers rely on several forms, derive the double from typeof dependency and add cases for each supported call. For a generic dependency, decide whether the subject needs the full generic relationship or one concrete specialization. A broad mock that accepts the union of every imaginable input is not equivalent to preserving how input and output types relate.
Async contracts are more than Promise labels. mockResolvedValue should receive the resolved production type, while a synchronous mockReturnValue should be rejected for a promise-returning function unless that runner API wraps it intentionally. Generator, callback, and abort-signal behavior also belong to the callable type. Keeping those pieces visible prevents a test fake from using an execution model the subject could never receive.
The Jest mock function reference documents inferred implementations, generic function types, and jest.mocked. The Vitest vi reference provides the corresponding tools. A strict project configuration makes their signals more useful; see enabling TypeScript strict mode in test frameworks.
How Do Unsafe Casts Hide Mock Regressions?
The repository records a concrete failure in src/db/seed-data/challenges/expansion-js-ts.ts. Inside JS_TS_CHALLENGES, the entry with slug javascript-testing-typed-mocks-scenario is titled "The Cast That Smuggled a Regression." A refactor renames User.name to User.displayName, while a stale test keeps returning { name: "Ada Lovelace", plan: "pro" }.
Two assertions remove every place where the compiler could object:
const fetchUserMock = fetchUser as jest.Mock;
This turns a known fetchUser signature into a broad runner type. Its mockResolvedValue no longer needs to follow Promise<User>.
{ name: "Ada Lovelace", plan: "pro" } as unknown as User
This first moves the literal to unknown, then commands TypeScript to treat it as User. The compiler is not asked whether displayName exists. The test may remain green because another output location still contains the old name property, even while production renders an undefined display name.
The key lesson is not that TypeScript missed a cross-file refactor. The test explicitly removed the connection. The repository scenario repairs the function with jest.mocked(fetchUser) and repairs data through makeUser(overrides: Partial<User>): User or a checked literal using satisfies User.
Use TypeScript control-flow narrowing for assertion helpers when a runtime check should establish a safe type. Narrowing follows evidence. A double assertion does the opposite: it skips evidence.
The same concern applies to as any, broad runner mock casts, and untyped JSON fixtures. A comment may explain why a rare invalid-state test crosses a boundary, but ordinary valid fixtures should compile against the production contract.
jest.mocked, vi.mocked, and Typed fn: Which Tool Should You Use?
Choose based on where the double comes from. mocked helpers are for imports or objects that the runner has already replaced or spied on. fn creates a new double, which works well with dependency injection. Supplying an implementation often gives strong inference; supplying a function type is useful when the implementation is configured later.
| Tool | Starting value | Contract retained | Runner | Best use |
|---|---|---|---|---|
jest.mocked(source) | imported mocked value | source function or object type | Jest | expose mock methods after module mocking |
vi.mocked(source) | imported mocked value | source function or object type | Vitest | same role in a Vitest suite |
jest.fn<typeof dep>() | new double | dependency function type | Jest | injected dependency configured later |
vi.fn<typeof dep>() | new double | dependency function type | Vitest | injected dependency configured later |
vi.fn((arg: T) => value) | typed implementation | inferred parameters and result | Vitest | concise behavior-specific dependency |
untyped fn() | new broad double | little until configured | either | avoid when contract matters |
Deep mocked object helpers may transform nested members in the type view. That does not make nested production values exist at runtime, and it does not automatically choose correct behavior. Prefer the smallest mocking surface that the subject actually consumes.
For imported module format concerns, read debugging ESM and CommonJS conflicts in TypeScript tests. Module replacement must succeed at runtime before jest.mocked() or vi.mocked() can provide a useful typed handle. The typing helper does not perform module mocking by itself.
If the subject accepts a dependency parameter, a typed fn may be cleaner than global module mocking. The dependency contract is visible, test isolation improves, and each case can supply a distinct implementation without relying on import timing.
A Numbered Workflow for Replacing Unsafe Test Casts
Refactor one dependency boundary at a time:
- Find the production function or interface and record its complete current signature.
- Remove
as jest.Mock,as vi.Mock,as any, oras unknown as Tfrom the test setup. - Use
jest.mocked()orvi.mocked()for an imported value that the runner has replaced. - Use
jest.fn<typeof dependency>(),vi.fn<typeof dependency>(), or a typed implementation for a new double. - Make every configured return value compile against the real result type.
- Centralize complete fixture defaults in a factory and accept only narrow
Partial<T>overrides. - Use
satisfiesfor literal fixtures whose precise inferred properties are useful. - Add a compile-failure example with
@ts-expect-erroronly when documenting the rejected contract. - Run
tsc --noEmitand the runtime test command in CI as separate required checks. - Review whether the mock behavior still represents production failure, success, timing, and side-effect cases.
Do not replace one broad assertion with a more complicated broad assertion. as jest.MockedFunction<any> still includes any. A generic wrapper that returns T by casting an object may simply move the hole into shared code. The factory implementation itself must construct a complete valid value.
Keep the type-check command responsible for test files. Confirm that the relevant tests are included by the TypeScript project used in CI. A typed mock offers no protection if the test file is excluded from compilation.
Use compiler failures as migration inventory. After removing a broad cast, collect each error by category: stale parameters, invalid fixture shape, wrong async result, or runner setup. Repair the production relationship rather than suppressing the message. If many files share the same stale fixture, introduce one complete factory and move cases gradually. If the same imported function is cast repeatedly, expose one typed mock handle in the suite setup without exporting it as an unconstrained generic.
Review call assertions too. A mock may accept correct arguments while the test asserts only call count. Add toHaveBeenCalledWith where values carry behavior, but do not snapshot every incidental option. Type checking proves the asserted values are assignable; runtime call assertions prove the subject actually supplied them.
The strict Playwright TypeScript framework guide covers suite-wide configuration patterns. The mock workflow here is runner-agnostic and applies to Jest or Vitest unit tests as long as TypeScript checks the files.
Code Example: Replace as jest.Mock with jest.mocked
The following example rebuilds the repository scenario from the javascript-testing-typed-mocks-scenario entry in JS_TS_CHALLENGES. It retains the imported fetchUser signature and uses complete checked data.
import { expect, jest, test } from "@jest/globals";
import { fetchUser } from "./api";
import { renderUserCard } from "./user-card";
import type { User } from "./types";
jest.mock("./api");
const fetchUserMock = jest.mocked(fetchUser);
function makeUser(overrides: Partial<User> = {}): User {
return {
displayName: "Ada Lovelace",
plan: "pro",
...overrides,
};
}
const freeUser = {
displayName: "Grace Hopper",
plan: "free",
} satisfies User;
test("renders the display name", async () => {
fetchUserMock.mockResolvedValue(makeUser());
const html = await renderUserCard("u_42");
expect(fetchUserMock).toHaveBeenCalledWith("u_42");
expect(html).toContain("Ada Lovelace");
});
test("renders a free user", async () => {
fetchUserMock.mockResolvedValue(freeUser);
expect(await renderUserCard("u_7")).toContain("Grace Hopper");
});
// After the rename, this remains a compile error:
// @ts-expect-error User requires displayName, not the removed name property.
fetchUserMock.mockResolvedValue({ name: "Stale", plan: "pro" });jest.mocked(fetchUser) does not change the runtime value by itself; jest.mock("./api") performs module replacement. The helper supplies the typed view that connects mock methods to the imported function. mockResolvedValue must now accept the resolved type of the production promise.
The @ts-expect-error line is documentation for a deliberate negative type example, not production test setup. TypeScript will also complain if the line unexpectedly stops producing an error, which helps keep educational examples accurate. Remove such examples if they create noise in the suite.
The freeUser literal keeps its precise inferred properties while TypeScript checks compatibility. The TypeScript 4.9 notes explain the satisfies operator. For broader fixture configuration patterns, see using satisfies for type-safe test configuration.
How Should You Build Complete Fixtures with Partial Overrides?
Place Partial<User> at the override input, not at the factory output. The defaults must be complete, the merge must return User, and consumers should receive the same shape they would receive in production.
This factory boundary creates useful refactor pressure. If production adds a required locale, the factory stops compiling until a valid default is chosen. One repair updates all tests that use the factory. Individual cases can still override locale when behavior depends on it.
Be careful with nested objects. A shallow Partial<User> makes top-level properties optional but does not make nested fields recursively optional. That is often desirable because it avoids constructing half-valid nested values. If a test needs nested customization, add a nested factory or a purpose-built override type rather than applying an unchecked deep merge.
Spread order is part of the factory contract. Defaults must come first and overrides last when callers are allowed to replace fields. If required fields must never become undefined, the override type should prevent that under the project's optional-property settings, or the factory should validate and reconstruct those fields explicitly. Do not rely on a return annotation to make a runtime undefined disappear; annotations check assignability of source expressions but do not repair values.
Keep identifiers and timestamps deterministic unless uniqueness is the behavior under test. Random defaults make snapshots and failure reproduction harder, while current-time defaults can blur whether a value came from the scenario or test clock. A complete factory can accept explicit overrides for these fields and still use readable stable defaults for ordinary cases.
Factories should model valid domain states, not every possible invalid input. Test malformed external data before it becomes User, through the parser or validator that production uses. If the domain itself represents an invalid or transitional state as a tagged union, add a properly typed factory for that variant instead of mutating a valid fixture through a cast.
The TypeScript utility reference defines Partial<Type> as a type with optional properties. Optional does not mean validated at runtime, and it does not make arbitrary keys legal. Excess-property checks and satisfies can still catch misspellings on object literals.
Compare these boundaries:
| Pattern | Compile-time result | Maintenance effect |
|---|---|---|
makeUser(overrides: Partial<User>): User | complete output required | central repair point |
makeUser(): Partial<User> | missing fields spread to callers | weak consumer contract |
literal satisfies User | exact literal checked | good for named fixed cases |
literal as User | compatibility may be forced | refactor defects can hide |
| external data parsed at runtime | value checked at boundary | appropriate for untrusted fixtures |
The TypeScript utility types for test data builders explores builder design in more depth. The generics guide for reusable fixtures helps when factories share safe patterns across several models.
Do not let factory defaults erase scenario meaning. A case testing a suspended account should state status: "suspended" explicitly even if the default is active. Type safety keeps the value valid; readable overrides keep the behavior intentional.
Code Example: Keep a Vitest Dependency Mock Signature Intact
src/lib/companion/openrouter-core.test.ts supplies a cast-free example. Its openWithFallback tests define generator helpers for success, empty output, immediate failure, and mid-stream failure. Each open double uses vi.fn((model: string) => ...), so argument inference and generator results remain connected to the implementation.
The adapted example below names the dependency type and retains the exact behaviors exercised by openWithFallback.
import { describe, expect, it, vi } from "vitest";
import {
OpenRouterError,
openWithFallback,
} from "./openrouter-core";
type OpenModel = (model: string) => AsyncGenerator<string>;
async function* chunks(...parts: string[]): AsyncGenerator<string> {
for (const part of parts) yield part;
}
async function* failsImmediately(
model: string,
): AsyncGenerator<string> {
if (model) throw new OpenRouterError(model, 500);
yield "";
}
describe("openWithFallback", () => {
it("uses a typed fallback after an immediate primary failure", async () => {
const open = vi.fn<OpenModel>((model) =>
model === "primary" ? failsImmediately(model) : chunks("saved"),
);
const result = await openWithFallback({
primary: "primary",
fallback: "fallback",
open,
});
expect(result.model).toBe("fallback");
const received: string[] = [];
for await (const part of result.textStream) received.push(part);
expect(received).toEqual(["saved"]);
expect(open).toHaveBeenNthCalledWith(1, "primary");
expect(open).toHaveBeenNthCalledWith(2, "fallback");
});
});
// These examples fail compilation if uncommented:
// vi.fn<OpenModel>((model: number) => chunks(String(model)));
// vi.fn<OpenModel>(() => Promise.resolve("not a generator"));The type states that open accepts a model string and returns an asynchronous generator of strings. An incompatible argument or promise-returning implementation is rejected before the runtime suite starts. The actual tests still need to prove fallback selection, stream delivery, call counts, and mid-stream behavior.
Different helper generators are preferable to one highly configurable fake here. chunks, empty, failsImmediately, and failsMidStream make behavior visible. Their explicit return annotations ensure each helper also follows the stream contract.
For errors caused by impossible narrowed types, debugging TypeScript never and exhaustiveness errors explains another compiler feedback path. Do not silence it with a cast until the control flow and intended contract are understood.
Typed Full Mocks vs Partial Dependencies vs Fixture Factories
Not every test needs a full mocked service. Sometimes the subject should depend on a smaller interface. If a function only needs open(model), accepting an OpenModel dependency is clearer than accepting a large provider client and constructing dozens of irrelevant mock methods.
| Technique | Contract retained | Setup cost | Refactor response | Runtime limitation | Suitable boundary |
|---|---|---|---|---|---|
| typed imported mock | module export signature | medium | import changes fail compilation | module behavior is replaced | existing module dependency |
| typed injected function | exact callable signature | low | parameters and results fail compilation | fake may be unrealistic | narrow operation |
| partial dependency interface | intentionally small interface | low | consumed members stay checked | can omit integration behavior | dependency inversion |
| complete fixture factory | production object shape | medium once | required fields fail centrally | default may hide scenario intent | repeated valid data |
checked literal with satisfies | complete target compatibility | low | local fixture fails | repeated setup | named fixed case |
| runtime parser | external value contract | higher | invalid data fails at runtime | needs parser maintenance | untrusted file or network data |
A partial dependency is not an incomplete production object forced through a cast. It is a real interface designed around what the subject consumes. TypeScript then checks both the subject and each supplied implementation against that narrower contract.
Use Pick only when it communicates an intentional stable capability. A local interface can be clearer when the subject's abstraction should not change every time the large vendor client changes. In either form, production wiring must adapt the real dependency to the narrow contract, which lets TypeScript verify that tests and runtime code agree. The test should not invent a smaller shape that production never accepts.
Spies are another option when retaining most real behavior is safe. A typed spy can replace one method while leaving the rest of an object intact, but it also carries shared-state and cleanup risks. Restore it after each case and avoid spying on network or persistent side effects merely to reduce setup. Choose based on the boundary the subject owns, not on which syntax is shortest.
Runtime validation is still necessary at external boundaries. The guide to runtime schema validation with static TypeScript models explains why a declared type cannot validate JSON loaded from disk or a response. Typed mocks address compile-time test code, not untrusted runtime data.
Exact optional-property behavior can also affect fixtures. Modeling exactOptionalPropertyTypes in API tests distinguishes an absent property from a property explicitly assigned undefined. A factory should follow the project's chosen semantics.
Which Casts Are Acceptable in TypeScript Tests?
Prefer no assertion when inference, a typed helper, satisfies, narrowing, or a factory can express the contract. Permit a narrow assertion at a checked boundary when runtime evidence exists but TypeScript cannot carry it. Permit deliberate construction of an inaccessible invalid state only when the test's purpose requires it, the scope is minimal, and a comment explains the boundary.
Reject as unknown as T, as any, and broad runner mock casts for ordinary valid fixtures. They suppress exactly the errors a typed test suite should surface. Also reject shared helpers that hide those assertions behind generic names such as mockValue<T>().
Use @ts-expect-error for intentional compile-failure examples because it documents that an error is required. Do not use it to make normal test setup compile. A test that verifies runtime handling of malformed external input should usually pass unknown data through the same parser used in production instead of pretending the value is already a domain type.
Require both commands in CI:
- A type-check such as
tsc --noEmitthat includes production and test sources. - The Jest or Vitest command that executes runtime behavior.
They detect different failures. Compilation finds stale signatures and shapes. Runtime tests find wrong branching, calls, effects, and results. A green runner does not imply that TypeScript checked the files, and a green compiler does not execute the scenario.
Add lint or review checks for the highest-risk escape hatches if the suite repeatedly regresses. A rule can flag explicit any or double assertions, but it cannot decide every legitimate boundary. Pair automation with a short policy: mocked imports use typed helpers, injected functions keep named signatures, ordinary fixtures are complete, and any suppressed type error explains the invalid state being tested.
Keep runner type packages and TypeScript versions aligned with configuration. An upgrade can alter helper inference even when runtime behavior stays the same. Run type checking before accepting generated snapshot or fixture updates so an apparent test-runner migration does not reintroduce broad casts as a shortcut.
Review the mock's return channel as carefully as its immediate value. A dependency returning Promise<User> needs a valid User in mockResolvedValue; a dependency returning AsyncGenerator<string> needs an implementation that yields strings and follows the expected failure timing. Replacing either with a loosely typed function can make a test model a state production cannot produce.
The repository's openWithFallback cases show why runtime variation still matters after signature checking. chunks() yields content, empty() completes without content, failsImmediately() throws before output, and failsMidStream() yields once before throwing. Each helper satisfies the generator contract, yet each drives a different branch. Type safety keeps the channel valid while runtime assertions prove fallback and propagation behavior.
Call assertions benefit from the same retained signature. When open accepts a model string, toHaveBeenCalledWith should use a valid model argument rather than a cast value. If production later adds a required options object, a typed double exposes stale calls, implementations, and expectations during compilation. A generic mock can leave all three outdated.
Check object factories for nested aliasing as well. A complete top-level return type does not guarantee that tests receive fresh mutable arrays or maps. If one test mutates a shared default, another can observe it. Create fresh nested values inside the factory where isolation matters, and let overrides replace them through an intentional boundary.
Keep factory names tied to valid domain states. makeUser() should return a usable User; a helper for malformed external input should return unknown or the raw transport shape and pass through production validation. Naming an invalid forced object makeUser() hides the difference between testing a domain object and testing boundary rejection.
During review, ask where the compiler would report a renamed required field. A healthy design points to the complete default, a satisfies literal, or the mocked result. If the answer is "nowhere until runtime," find the assertion or generic that removed the relationship. This question is more useful than counting casts without understanding their effect.
Advanced interview-style examples appear in TypeScript questions for senior SDETs, but the daily policy can remain short: retain real dependency types, construct complete values, and demand an explanation for any escaped check.
Frequently Asked Questions About TypeScript Typed Mocks
What is a typed mock in TypeScript?
It is a runner double that keeps the production callable or object contract while adding configuration and call history. Incompatible arguments, returns, and fixture shapes should fail compilation. Runtime assertions still prove what the subject did with the configured behavior.
When should I use jest.mocked instead of jest.Mock?
Use jest.mocked after Jest replaces an imported function or object and you need correctly typed mock methods. A broad jest.Mock cast can lose the source signature. Use jest.fn<typeof dependency>() when creating a new injected double.
What is the Vitest equivalent of jest.mocked?
Use vi.mocked for an already mocked import. Use vi.fn with a function type or typed implementation for a new double. The repository's openWithFallback tests demonstrate inference from vi.fn((model: string) => ...).
Why is as unknown as unsafe in test fixtures?
It bypasses structural compatibility instead of establishing it. Missing required fields and stale renamed fields can pass compilation. A complete factory, a satisfies literal, or runtime parsing at an unknown-data boundary preserves evidence.
Should a mock factory return Partial of the production type?
Usually it should accept Partial<T> overrides and return complete T. That centralizes valid defaults and keeps consumers honest. A partial return is appropriate only when the real consumer contract is intentionally partial.
Do typed mocks remove the need for runtime tests?
No. Types constrain setup but cannot prove call order, retries, side effects, thrown errors, stream timing, or realistic scenarios. Run type checking and the test runner as separate gates, and review whether each fake behavior represents the production boundary.
Next Steps: Make Mock Drift a Compile Failure
Search test sources for broad runner casts, double assertions, as any, and factories that return Partial without a partial production contract. Start with one high-value dependency. Restore its source signature through mocked or typed fn, then make every result fixture compile without force.
Add complete factories for repeated domain values, keep scenario-relevant overrides explicit, and include test files in tsc --noEmit. Preserve runtime cases such as the openWithFallback success, empty, immediate-failure, and mid-stream paths. The finished suite should fail early when types drift and still fail behaviorally when a validly typed double drives the wrong result.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official jestjs.io reference
jestjs.io
Primary documentation selected and verified for the claims in this guide.
- 02Official vitest.dev reference
vitest.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official typescriptlang.org reference
typescriptlang.org
Primary documentation selected and verified for the claims in this guide.
- 04Official typescriptlang.org reference
typescriptlang.org
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
What is a typed mock in TypeScript?
A typed mock retains the production function's parameters, return value, promise behavior, and relevant object shape while adding runner methods for configuring and inspecting calls. TypeScript should reject incompatible arguments or results in the test. Runtime assertions are still required because types cannot prove that configured behavior represents production.
When should I use jest.mocked instead of jest.Mock?
Use jest.mocked when an imported function or object has been replaced by Jest and you need mock methods without losing its real signature. A broad jest.Mock annotation can discard parameter and return contracts. For a new standalone double, jest.fn with the dependency function type is often the clearer choice.
What is the Vitest equivalent of jest.mocked?
Vitest provides vi.mocked for an already mocked import and vi.fn for a new mock function. Passing a typed implementation to vi.fn lets TypeScript infer the parameters and return shape. Choose the helper that matches whether the production import was mocked or a dependency is injected directly into the subject.
Why is as unknown as unsafe in test fixtures?
The double assertion tells TypeScript to accept a value without proving its structural compatibility with the target. A stale fixture can therefore omit a required field or retain a renamed field while compilation stays green. Use a complete factory, satisfies, or validated boundary parsing so changed production types fail tests during compilation.
Should a mock factory return Partial of the production type?
Usually no. Accept Partial of the type as an override input, merge it with complete valid defaults, and return the full production type. Returning Partial moves optionality into every consumer and can hide missing required values. Use a partial return only when the production dependency itself accepts a genuinely partial object.
Do typed mocks remove the need for runtime tests?
No. Types reject incompatible setup, but they do not prove call order, retry behavior, error handling, branching, side effects, or the realism of a chosen fixture. Keep runtime assertions for behavior and use compilation as an additional gate. Typed setup narrows the ways a test can become stale without warning.
RELATED GUIDES
Continue the learning route
GUIDE 01
Use TypeScript satisfies for Type-Safe Test Configuration
Master TypeScript satisfies test config with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 02
TypeScript Utility Types for Test Data Builders
Master TypeScript utility types test data with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 03
Enable TypeScript Strict Mode in Test Frameworks
Master TypeScript strict mode test framework with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 04
Strict TypeScript Patterns for Playwright Fixtures, Models, and Test Data
Apply strict TypeScript to Playwright fixtures, models, environment data, and case tables with compiler checks, runtime validation, and safe narrowing.
GUIDE 05
TypeScript Generics for Reusable Test Fixtures
Master TypeScript generics test fixtures with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 06
Debug TypeScript never and Exhaustiveness Errors
Master debug TypeScript never errors with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.