TypeScript 93 ๐ท TypeScript with Testing โ Jest and Vitest
Writing tests in TypeScript is not the same as writing them in JavaScript. The type system changes what you can assert, how you mock dependencies, and what happens when the test fails. A test that compiles is not necessarily a test that runs. A mock that satisfies the compiler may not satisfy the runtime. Jest and Vitest are the two test runners that dominate the TypeScript ecosystem, and they approach these problems differently.
Jest is the older framework. It was built for JavaScript, and TypeScript support arrived through a transformer โ ts-jest or Babel’s TypeScript preset. The setup works, but it adds a configuration layer that duplicates what your build tool is already doing. Vitest was built for Vite, and Vite already understands TypeScript. Vitest reuses the same transform pipeline, so there is no separate configuration for tests. The API is nearly identical โ describe, it, expect, vi.fn() instead of jest.fn() โ which makes migration mostly mechanical .
Key point: Jest and Vitest both type-check your tests only if you ask them to. Jest relies on ts-jest to transform and optionally type-check. Vitest uses Vite’s transform pipeline and separates runtime tests from type tests. Vitest’s type-checking mode runs tsc or vue-tsc and reports type errors as test failures, but only for files that match the typecheck.include pattern . Neither runner validates your types by default. The compiler is still the compiler.
Why testing frameworks matter for TypeScript
A TypeScript test has three layers: the test code itself, the code under test, and the types that connect them. The framework determines how the first two are compiled and executed. The types are checked separately.
The transform problem. Jest cannot execute TypeScript directly. It needs a transformer โ ts-jest, babel-jest with @babel/preset-typescript, or @swc/jest โ to convert .ts files to JavaScript before running them. This transformer is configured separately from your application’s build. If the transformer and the build disagree about which TypeScript features are enabled, the test and the application may behave differently . Vitest avoids this by reusing Vite’s transform. The same plugins, the same tsconfig.json, the same settings that build your application also run your tests .
The type-checking problem. TypeScript types are erased before execution. A test that passes a string where a number is expected will compile if you use as any, and it will run. The type error never reaches the runtime. To catch type errors in tests, you need a separate check. Vitest provides vitest typecheck (or vitest --typecheck) which runs the TypeScript compiler against .test-d.ts files and reports type mismatches as test failures . Jest does not have a built-in equivalent. The tsc --noEmit command runs separately.
The mocking problem. TypeScript’s type system and Jest’s mock system do not naturally align. jest.fn() returns a mock whose type is inferred from the implementation, or any if no implementation is given. To type a mock properly, you use jest.Mocked<T> or jest.MockedFunction<T>. Vitest has the same pattern with vi.fn() and Mocked<T> from vitest . The type definitions differ slightly, but the concept is the same.
The trade-off. Jest is mature, widely used, and has years of documentation and community plugins. Vitest is newer, faster in Vite projects, and has a more modern architecture. For a project already using Vite, Vitest is the natural choice. For a project using Jest successfully, migration is optional. The API compatibility means the cost of switching is low if you ever need to .
a. Setting Up TypeScript Tests
The setup for Jest and Vitest differs in how the TypeScript transform is configured.
Jest with ts-jest requires a transformer entry in jest.config.js. The ts-jest preset reads tsconfig.json and applies the settings to test files. The isolatedModules option speeds up transformation by skipping type checking, which is then done separately by tsc .
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }],
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
export default config;
The isolatedModules: true setting tells ts-jest to transform each file independently without type checking. This is faster but means type errors in tests are not reported by Jest. The tsc --noEmit command is still required to catch them.
Vitest needs no transformer configuration. It reads vite.config.ts and applies the same plugins. The test configuration lives in a test block within the Vite config, or in a separate vitest.config.ts that extends it .
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
The globals: true setting makes describe, it, and expect available without importing them, mimicking Jest’s default behavior. Vitest does not enable globals by default; the setting must be turned on explicitly. With globals disabled, tests import from vitest: import { describe, it, expect } from 'vitest' .
For TypeScript to recognize the globals, the tsconfig.json must include "types": ["vitest/globals"]. Without this, the compiler reports Cannot find name 'describe' in test files . The same applies to Jest: "types": ["jest"] is needed if globals are used and @types/jest is not auto-discovered.
b. Typed Mocks and Spies
The mock system is where TypeScript’s type system and the test runner’s runtime meet. A mock replaces a real dependency, and TypeScript must know what the replacement is allowed to do.
Jest’s jest.fn() creates a mock function. Without type arguments, the return type is jest.Mock<any, any>. The jest.Mocked<T> utility type wraps an object so that every method is a mock . The jest.MockedFunction<T> utility type does the same for a single function.
import { jest } from '@jest/globals';
interface UserService {
getUser(id: string): Promise<User>;
saveUser(user: User): Promise<void>;
}
const mockUserService: jest.Mocked<UserService> = {
getUser: jest.fn(),
saveUser: jest.fn(),
};
mockUserService.getUser.mockResolvedValue({ id: '1', name: 'Alice' });
The jest.Mocked<UserService> type ensures that getUser and saveUser are both mock functions. The mockResolvedValue method is available on getUser because jest.fn() returns a mock that knows the return type is Promise<User>.
Vitest’s vi.fn() follows the same pattern. The Mocked<T> type from vitest wraps an object. The MockedFunction<T> type wraps a function .
import { vi, type Mocked } from 'vitest';
interface UserService {
getUser(id: string): Promise<User>;
saveUser(user: User): Promise<void>;
}
const mockUserService: Mocked<UserService> = {
getUser: vi.fn(),
saveUser: vi.fn(),
};
mockUserService.getUser.mockResolvedValue({ id: '1', name: 'Alice' });
The difference is the namespace: jest becomes vi, and jest.Mocked becomes Mocked. The rest is identical. The codemod for migrating from Jest to Vitest handles these renames automatically, though vi.hoisted() is required for mocks that reference variables in their factory functions .
The hoisting trap. Both Jest and Vitest hoist mock() calls to the top of the file. This means the factory function cannot reference variables declared later in the file. In Jest, this works because the module registry is set up differently. In Vitest, the factory function is evaluated in a different scope, and referencing outer variables produces undefined errors. The fix is vi.hoisted(), which explicitly creates values that are available in the hoisted scope .
// โ Fails in Vitest
const mockUser = { id: '1' };
vi.mock('./user-service', () => ({
getUser: () => mockUser, // mockUser is undefined at hoist time
}));
// โ
Works in Vitest
const mockUser = vi.hoisted(() => ({ id: '1' }));
vi.mock('./user-service', () => ({
getUser: () => mockUser,
}));
This is the single most common migration failure. The Vitest documentation states it plainly: roughly 40% of vi.mock() calls with factory functions need vi.hoisted() .
c. Type Testing with Vitest
Vitest has a feature that Jest does not: built-in type testing. You can write tests that assert on types rather than values, and Vitest runs them through tsc as part of the test suite .
Type tests live in .test-d.ts files. They use expectTypeOf or assertType from vitest. The assertions are checked by the TypeScript compiler, not by the runtime.
// user.test-d.ts
import { expectTypeOf, assertType } from 'vitest';
import { createUser } from './user';
test('createUser returns a User', () => {
expectTypeOf(createUser).returns.toEqualTypeOf<User>();
});
test('createUser rejects invalid input', () => {
// @ts-expect-error name is required
assertType(createUser({}));
});
The expectTypeOf API provides matchers like toBeString(), toBeNumber(), toEqualTypeOf<T>(), and toExtend<T>(). When a type assertion fails, Vitest prints the type mismatch with the expected and actual types .
The @ts-expect-error directive is the standard way to assert that a type error should occur. If the line compiles without error, @ts-expect-error itself becomes an error, indicating that the type test failed.
Type tests are not run by the default vitest command. They require vitest typecheck or vitest --typecheck. The typecheck.include configuration option controls which files are treated as type tests. Since Vitest 2.1, runtime tests and type tests are reported as separate entries if they overlap .
The Jest alternative. Jest does not have built-in type testing. The equivalent is to write a separate TypeScript file that uses type assertions and run tsc --noEmit against it. The expect-type library provides expectTypeOf outside of Vitest, but it must be integrated into the build manually. Vitest’s advantage is that type tests and runtime tests share the same runner and the same report.
Complete Example Session
This session builds a TypeScript project with Jest configuration, Vitest configuration, typed mocks, and a type test.
// ============================================
// PART 1: THE CODE UNDER TEST
// ============================================
// src/calculator.ts
export type Operation = 'add' | 'subtract' | 'multiply' | 'divide';
export interface CalculatorInput {
operation: Operation;
a: number;
b: number;
}
export interface CalculatorResult {
operation: Operation;
result: number;
}
export function calculate(input: CalculatorInput): CalculatorResult {
let result: number;
switch (input.operation) {
case 'add':
result = input.a + input.b;
break;
case 'subtract':
result = input.a - input.b;
break;
case 'multiply':
result = input.a * input.b;
break;
case 'divide':
if (input.b === 0) {
throw new Error('Division by zero is not allowed');
}
result = input.a / input.b;
break;
}
return { operation: input.operation, result };
}
// ============================================
// PART 2: THE JEST CONFIGURATION
// ============================================
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }],
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
export default config;
// ============================================
// PART 3: THE VITEST CONFIGURATION
// ============================================
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
typecheck: {
include: ['src/**/*.test-d.ts'],
tsconfig: './tsconfig.json',
},
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
// ============================================
// PART 4: THE JEST TEST WITH TYPED MOCKS
// ============================================
// src/calculator.test.ts (Jest)
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { calculate } from './calculator';
describe('calculate', () => {
it('adds two numbers', () => {
const result = calculate({ operation: 'add', a: 1, b: 2 });
expect(result).toEqual({ operation: 'add', result: 3 });
});
it('throws on division by zero', () => {
expect(() => calculate({ operation: 'divide', a: 1, b: 0 }))
.toThrow('Division by zero is not allowed');
});
});
// ============================================
// PART 5: THE VITEST TEST WITH TYPED MOCKS
// ============================================
// src/calculator.test.ts (Vitest)
import { describe, it, expect } from 'vitest';
import { calculate } from './calculator';
describe('calculate', () => {
it('adds two numbers', () => {
const result = calculate({ operation: 'add', a: 1, b: 2 });
expect(result).toEqual({ operation: 'add', result: 3 });
});
it('throws on division by zero', () => {
expect(() => calculate({ operation: 'divide', a: 1, b: 0 }))
.toThrow('Division by zero is not allowed');
});
});
// ============================================
// PART 6: THE TYPED MOCK WITH VI
// ============================================
// src/user-service.test.ts
import { describe, it, expect, vi, type Mocked } from 'vitest';
interface Database {
insert(collection: string, data: unknown): Promise<number>;
findById(collection: string, id: number): Promise<unknown>;
}
class UserService {
constructor(private db: Database) {}
async createUser(data: unknown): Promise<unknown> {
const id = await this.db.insert('users', data);
return { id, ...data as object };
}
}
describe('UserService', () => {
it('inserts user data', async () => {
const mockDb: Mocked<Database> = {
insert: vi.fn(),
findById: vi.fn(),
};
mockDb.insert.mockResolvedValue(123);
const service = new UserService(mockDb);
const result = await service.createUser({ name: 'Alice' });
expect(mockDb.insert).toHaveBeenCalledWith('users', { name: 'Alice' });
expect(result).toEqual({ id: 123, name: 'Alice' });
});
});
// ============================================
// PART 7: THE TYPE TEST
// ============================================
// src/calculator.test-d.ts
import { expectTypeOf, assertType } from 'vitest';
import { calculate, CalculatorInput, CalculatorResult } from './calculator';
test('calculate returns CalculatorResult', () => {
expectTypeOf(calculate).returns.toEqualTypeOf<CalculatorResult>();
});
test('calculate accepts valid input', () => {
const input: CalculatorInput = { operation: 'add', a: 1, b: 2 };
assertType<CalculatorResult>(calculate(input));
});
test('calculate rejects invalid operation', () => {
// @ts-expect-error 'power' is not a valid Operation
calculate({ operation: 'power', a: 1, b: 2 });
});
// ============================================
// PART 8: THE MOCK HOISTING FIX
// ============================================
// โ Fails in Vitest
const mockUser = { id: '1', name: 'Alice' };
vi.mock('./user-service', () => ({
getUser: () => mockUser,
}));
// โ
Works in Vitest
const mockUser = vi.hoisted(() => ({ id: '1', name: 'Alice' }));
vi.mock('./user-service', () => ({
getUser: () => mockUser,
}));
// ============================================
// PART 9: THE TSCONFIG FOR TESTS
// ============================================
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["vitest/globals"],
"lib": ["ES2022"]
},
"include": ["src/**/*.ts"]
}
// For Jest, use:
// "types": ["jest"]
// ============================================
// PART 10: THE PACKAGE SCRIPTS
// ============================================
// package.json (Vitest)
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"typecheck": "vitest typecheck",
"typecheck:tsc": "tsc --noEmit"
}
}
// package.json (Jest)
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"typecheck": "tsc --noEmit"
}
}
The ten parts cover the code under test, the Jest configuration, the Vitest configuration, the Jest test, the Vitest test, the typed mock, the type test, the mock hoisting fix, the tsconfig.json, and the package scripts.
Quick Reference
The Framework Comparison
| Feature | Jest | Vitest |
|---|---|---|
| Transform | ts-jest, Babel, SWC | Vite (esbuild) |
| Native ESM | Partial, needs config | Yes |
| Native TypeScript | Needs transformer | Yes |
| Mock namespace | jest | vi |
| Globals default | Enabled | Disabled |
| Type testing | External (tsc) | Built-in (vitest typecheck) |
| Coverage provider | Istanbul | v8 (default), Istanbul |
| UI mode | Third-party | Built-in (vitest --ui) |
| Watch mode | Available | Default, HMR-based |
The Mock Utilities
| Jest | Vitest | Purpose |
|---|---|---|
jest.fn() | vi.fn() | Create mock function |
jest.spyOn() | vi.spyOn() | Spy on method |
jest.mock() | vi.mock() | Mock module |
jest.requireActual() | vi.importActual() | Import original |
jest.Mocked<T> | Mocked<T> | Typed mock object |
jest.MockedFunction<T> | MockedFunction<T> | Typed mock function |
jest.useFakeTimers() | vi.useFakeTimers() | Fake timers |
The Type Testing API
| Matcher | Purpose |
|---|---|
expectTypeOf(x).toBeString() | Assert type is string |
expectTypeOf(x).toBeNumber() | Assert type is number |
expectTypeOf(fn).returns.toEqualTypeOf<T>() | Assert return type |
expectTypeOf(x).parameter(0).toExtend<T>() | Assert parameter type |
assertType<T>(value) | Assert value is T |
@ts-expect-error | Assert that a type error occurs |
The Migration Gotchas
| Gotcha | Fix |
|---|---|
vi.mock() factory references outer variable | Use vi.hoisted() |
jest.fn() not renamed | Codemod jest.* โ vi.* |
| Globals not available | Enable globals: true or import from vitest |
@testing-library/jest-dom missing | Use @testing-library/jest-dom/vitest |
moduleNameMapper regex fails | Use Vite resolve.alias |
done() callback | Refactor to async/await |
Best Practices
โ Do This:
// Use typed mocks with Mocked<T>
const mockService: Mocked<Service> = { method: vi.fn() }; // โ
// Use vi.hoisted for factory references
const mock = vi.hoisted(() => ({ value: 1 })); // โ
// Add type testing for critical APIs
expectTypeOf(calculate).returns.toEqualTypeOf<CalculatorResult>(); // โ
// Run tsc --noEmit alongside tests
"typecheck": "tsc --noEmit" // โ
// Enable globals and add types to tsconfig
"globals": true, "types": ["vitest/globals"] // โ
โ Don’t Do This:
// Don't use jest.* in Vitest
jest.fn() // use vi.fn() // โ
// Don't reference outer variables in vi.mock factory
const mock = { id: 1 };
vi.mock('./mod', () => ({ get: () => mock })); // undefined // โ
// Don't rely on Jest globals in Vitest without enabling
describe('test', () => {}); // fails without globals // โ
// Don't assume type errors are caught by test runner
// They are not checked at runtime. // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Cannot find name 'describe' | Globals not enabled or types not configured | Add globals: true and "types": ["vitest/globals"] |
| Mock factory returns undefined | Vitest hoisting scoping | Use vi.hoisted() |
| Type test not running | .test-d.ts not in typecheck.include | Add to config |
Mock type is any | No type annotation on vi.fn() | Use Mocked<T> or MockedFunction<T> |
| Coverage missing | Provider not installed | Install @vitest/coverage-v8 or @vitest/coverage-istanbul |
| Jest slow on ESM | Transformer overhead | Use Vitest or @swc/jest |
Real-World Examples
1. Jest Configuration
const config: Config = { preset: 'ts-jest', testEnvironment: 'node' };
2. Vitest Configuration
export default defineConfig({ test: { globals: true, environment: 'node' } });
3. Typed Mock (Vitest)
const mockDb: Mocked<Database> = { insert: vi.fn(), findById: vi.fn() };
4. Mock with Resolved Value
mockDb.insert.mockResolvedValue(123);
5. Type Test
expectTypeOf(calculate).returns.toEqualTypeOf<CalculatorResult>();
6. Assert Type Error
// @ts-expect-error invalid operation
calculate({ operation: 'power', a: 1, b: 2 });
7. Hoisted Mock
const mock = vi.hoisted(() => ({ id: '1' }));
vi.mock('./service', () => ({ getUser: () => mock }));
8. Spy on Method
const spy = vi.spyOn(service, 'getUser').mockResolvedValue(user);
9. Coverage Command
vitest run --coverage
10. Type Check Command
vitest typecheck
Visual
The Jest Transform Pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JEST + ts-jest โ
โ โ
โ test.ts โโ> ts-jest โโ> JS โโ> Jest runtime โ
โ โ โ
โ โโ type checking โ
โ (optional) โ
โ โ
โ Configuration: โ
โ jest.config.ts + tsconfig.json โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Vitest Transform Pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ VITEST + VITE โ
โ โ
โ test.ts โโ> Vite (esbuild) โโ> JS โโ> run โ
โ โ โ
โ โโ SAME pipeline as โ
โ your app build โ
โ โ
โ Configuration: โ
โ vitest.config.ts (extends vite.config.ts) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Mock Hoisting Difference
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JEST HOISTING โ
โ โ
โ const mock = { id: 1 }; โ
โ jest.mock('./mod', () => ({ โ
โ get: () => mock // works โ
โ })); โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ VITEST HOISTING โ
โ โ
โ const mock = { id: 1 }; โ
โ vi.mock('./mod', () => ({ โ
โ get: () => mock // โ undefined โ
โ })); โ
โ โ
โ โ
Fix: โ
โ const mock = vi.hoisted(() => ({ id: 1 })); โ
โ vi.mock('./mod', () => ({ โ
โ get: () => mock โ
โ })); โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Type Testing Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ VITEST TYPE TESTING โ
โ โ
โ calculator.test-d.ts โ
โ โ โ
โ โผ โ
โ vitest typecheck โ
โ โ โ
โ โผ โ
โ tsc / vue-tsc โ
โ โ โ
โ โผ โ
โ Type errors reported as test failures โ
โ โ
โ The file is NOT executed. โ
โ It is statically analyzed. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Jest transform | ts-jest, Babel, or SWC |
| Vitest transform | Vite (esbuild) |
| Mock namespace | jest vs vi |
| Typed mock | jest.Mocked<T> vs Mocked<T> |
| Type testing | Vitest built-in (vitest typecheck) |
| Hoisting fix | vi.hoisted() |
| Globals default | Jest: enabled, Vitest: disabled |
| Coverage | Istanbul vs v8 |
| Migration | Codemod jest.* โ vi.* |
Key takeaways:
- Jest requires a transformer for TypeScript; Vitest reuses Vite’s pipeline.
ts-jest, Babel, or SWC convert.tsfiles for Jest. Vitest uses the same esbuild transform that builds the application, eliminating configuration duplication . - The mock API is nearly identical, but the namespace changes.
jest.fn()becomesvi.fn(),jest.mock()becomesvi.mock(), andjest.Mocked<T>becomesMocked<T>. The codemod handles the mechanical renames, butvi.hoisted()is required for factory functions that reference outer variables . - Vitest has built-in type testing.
.test-d.tsfiles are checked bytscand reported as test failures.expectTypeOfandassertTypeprovide matchers for asserting on types. Jest does not have an equivalent built-in feature . - Globals are disabled by default in Vitest. Jest enables
describe,it, andexpectglobally. Vitest requiresglobals: truein the configuration and"types": ["vitest/globals"]intsconfig.jsonto match Jest’s behavior . - Typed mocks use
Mocked<T>to wrap the mock object. Without the type annotation,vi.fn()returns a mock whose type is inferred from the implementation orany. TheMocked<T>utility type ensures every method is a mock with the correct signature . - The hoisting difference is the most common migration failure. Vitest’s
vi.mock()factory functions are evaluated in a different scope than Jest’s. Referencing outer variables producesundefined.vi.hoisted()explicitly creates values in the hoisted scope . - Neither runner type-checks by default. Jest with
isolatedModulesskips type checking during transform. Vitest does not type-check runtime tests. Thetsc --noEmitcommand is still required to verify types, unless you use Vitest’stypecheckmode for type tests.
Remember: Jest and Vitest both run TypeScript tests, but they get there differently. Jest transforms TypeScript into JavaScript and runs the result. Vitest reuses the Vite pipeline that already understands TypeScript. The API is so similar that migration is mostly a find-and-replace, with one significant exception: vi.hoisted() for mock factories. Vitest adds type testing as a first-class feature. Jest relies on external tooling. For a Vite project, Vitest is the natural choice. For a Jest project, migration is optional. Either way, the types are checked by the compiler, not the runner.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!