Property-based Testing in TypeScript: How to Catch Bugs You Weren't Looking For

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Unit Tests Don't Save You From Everything

\n\n

Imagine you wrote an array sorting function. You covered it with a dozen unit tests: an empty array, a single element, an already sorted list, reverse order. The tests are green. You're confident in the code. But a week later, production crashes — on an array of 1000 elements with duplicates and negative numbers. Sound familiar?

\n\n

Traditional TypeScript testing using examples (example-based testing) only checks the scenarios you thought of. But what if a bug is hiding in a combination you didn't consider? This is where property-based testing comes in — an approach that automatically generates thousands of random inputs and checks the invariants of your code.

\n\n

In this article, we'll explore how to apply property-based testing in TypeScript with the fast-check library, which properties to check, and how this changes the development approach.

\n\n

What is Property-based Testing?

\n\n

Instead of writing:

\n
it('sorts [3, 1, 2] → [1, 2, 3]', () => {\n  expect(sort([3, 1, 2])).toEqual([1, 2, 3]);\n});\n
\n\n

You describe a property — an assertion that should be true for all valid inputs:

\n
it('a sorted array does not change the order of elements', () => {\n  fc.assert(\n    fc.property(fc.array(fc.integer()), (arr) => {\n      const sorted = sort(arr);\n      // Property 1: length is preserved\n      expect(sorted.length).toBe(arr.length);\n      // Property 2: each element of the original array is present\n      // Property 3: the array is sorted in ascending order\n      for (let i = 1; i < sorted.length; i++) {\n        expect(sorted[i - 1]).toBeLessThanOrEqual(sorted[i]);\n      }\n    })\n  );\n});\n
\n\n

The fast-check library will automatically generate hundreds of random arrays (including empty ones, with NaN, with fractional numbers, with huge values) and run your test on each. If even one fails — you'll know about the problem.

\n\n

Why is This Needed Specifically in TypeScript?

\n\n

TypeScript provides static typing, but it doesn't guarantee the correctness of logic. Property-based testing in TypeScript bridges the gap between "types match" and "the code works correctly." It is especially useful for:

\n\n
    \n
  • Data transformation functions (map, filter, reduce)
  • \n
  • Parsers and validators
  • \n
  • Algorithms with non-trivial logic
  • \n
  • Functions working with boundary values
  • \n
\n\n

The fast-check Library: Quick Start

\n\n

Install fast-check in your project:

\n
npm install --save-dev fast-check\n
\n\n

Basic syntax:

\n
import fc from 'fast-check';\n\ndescribe('properties of the reverse function', () => {\n  it('reverse(reverse(arr)) === arr', () => {\n    fc.assert(\n      fc.property(fc.array(fc.anything()), (arr) => {\n        expect(reverse(reverse(arr))).toEqual(arr);\n      })\n    );\n  });\n});\n
\n\n

Here fc.array(fc.anything()) is a generator of arbitrary arrays. You can combine generators:

\n
    \n
  • fc.integer() — integers
  • \n
  • fc.string() — strings
  • \n
  • fc.record({ name: fc.string(), age: fc.integer({ min: 0, max: 120 }) }) — objects
  • \n
  • fc.oneof(...) — random selection from multiple generators
  • \n
\n\n

Which Properties to Check? 5 Practical Examples

\n\n

1. Invariance (Reversibility)

\n

Any operation that has an inverse should be reversible:

\n
it('JSON serialization and deserialization', () => {\n  fc.assert(\n    fc.property(fc.object(), (obj) => {\n      const json = JSON.stringify(obj);\n      const parsed = JSON.parse(json);\n      expect(parsed).toEqual(obj);\n    })\n  );\n});\n<                

Blogs

Book Recommendations