If your Node tests are green but callers still need casts, you’re mixing two different guarantees. A Node test proves what the function did when executed: the values returned, whether the input was mutated, and which inputs throw. It does not and cannot prove how the TypeScript compiler will narrow that result at a later use site.

Implementation summary

  • Keep the runtime contract explicit: the helper should validate inputs (e.g., reject non-arrays) and return a new array that removes only null and undefined (use a nullish check rather than truthiness). The server-inserted example shows a small function that performs exactly that behavior and does not mutate the input.

Expected behavior to test

  • Mixed-array case: the output contains the original non-nullish items in order and preserves falsy-but-valid values like 0, '', and false.
  • Empty input: returns a new empty array (not the same reference).
  • All-nullish input: returns an empty array.
  • Invalid input: throws a TypeError for non-array arguments.

Where static checking belongs

  • If you need the helper to provide compiler-level narrowing (so consumers can assign without casts), verify that with the TypeScript compiler or editor diagnostics. A minimal compile-time check is to write a small use-site that only accepts the narrowed type and run tsc to ensure it typechecks.

Limitations and pitfalls

  • Compiler inference changes across TypeScript versions (e.g., TypeScript 5.5 adds inferred predicates in some filter callbacks). Relying on runtime tests to encode compiler behavior makes tests brittle or misleading.
  • Truthiness-based filters (e.g., !!x) can silently drop legitimate falsy values — runtime tests must check exact outputs, not just lengths.

Practical rule

  • Tests: prove the runtime contract you actually depend on. Compiler: prove the static narrowing your consumers rely on. Keep them separate so failures point to the right fix: change JS behavior, adjust types, or update consumers.

The article includes a repaired example and concrete test cases that illustrate this split and avoid overclaiming what a JavaScript test can prove. The server will append the full code example and the article URL.

Working example

export const note = `This module demonstrates runtime removal of null and undefined from arrays. Runtime tests (node:test) validate behavior at execution time but do NOT prove any TypeScript compile-time narrowing. To check static narrowing you must run the TypeScript compiler (tsc) or use editor/IDE type checks.`; /** * Remove null and undefined from an array while preserving order and other falsy * values such as 0, '', and false. * * Runtime contract: returns a new array containing only elements that are not * null and not undefined. Throws a TypeError if the argument is not an Array. * * Note: This demonstrates runtime behavior only. TypeScript's ability to * narrow types (e.g. from (T | null | undefined)[] to T[]) is a compile-time * property checked by tsc / the language service and is not proven by these * runtime tests. */ export function filterNonNullish(arr) { if (!Array.isArray(arr)) { throw new TypeError('filterNonNullish expects an Array'); } return arr.filter((x) => x != null); } 

Read the full article: https://chriseugenerodriguez.com/blog/tests-should-verify-runtime-contract-not-inferred-types