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
Top comments (0)