Typorithm · hard

12. A practical type-and-runtime test strategy

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

12. A practical type-and-runtime test strategy

0%

Concept and contract

Choose regression checks that fail for meaningful contract changes.

A useful verification plan separates static call contracts, runtime data behavior and user interaction. A compiler test can reject a mismatched payload; an executable test can detect incorrect filtering; a browser or native UI test can detect a button that never invokes the intended action.

Do not duplicate implementation line by line in tests. Choose independent expected outcomes and malformed inputs. Small deterministic fixtures are easier to understand than a large live-network suite that fails whenever a provider is unavailable.

Mechanism and worked example

The example’s filter accepts readonly roles and a finite country vocabulary.

The example’s filter accepts readonly roles and a finite country vocabulary. Positive tests should verify the returned ids, original ordering and unchanged input. A negative compiler fixture should reject a country outside the vocabulary rather than merely asserting that the function exists.

An expected-error directive requires some diagnostic on the next line, not a specific one. Keep negative lines tiny, and avoid accidentally satisfying the directive with an unrelated syntax or import error. Test a real consumer relationship that would break if the signature widened.

Complete worked example

typescript

type Role={id:string;country:"US"|"UA"};
function selectCountry(roles:readonly Role[],country:Role["country"]):Role[]{return roles.filter(role=>role.country===country);}
Reasoning
The finite country contract and immutable filtering behavior need distinct static and runtime checks.

Counterexamples and common mistakes

The rejected call uses an unsupported country.

The rejected call uses an unsupported country. If the parameter becomes string or any, this negative fixture should fail because the directive is unused. That is evidence the static contract changed, even if all current happy-path runtime tests still pass.

Do not execute intentionally rejected fixtures as ordinary demos. The directive suppresses a diagnostic but does not repair JavaScript behavior. Keep runtime assertions in accepted fixtures and type-error checks in separate compilation-only files.

Rejected counterexample: expected compiler diagnostic

typescript

type Role={id:string;country:"US"|"UA"};
function selectCountry(roles:readonly Role[],country:Role["country"]):Role[]{return roles.filter(role=>role.country===country);}
// @ts-expect-error Unsupported country vocabulary.
selectCountry([],"XX");
Reasoning
This tiny negative call should fail if the public vocabulary accidentally broadens.

Independent practice

Implement selectRemote for readonly roles and return only remote records in their original order.

Implement selectRemote for readonly roles and return only remote records in their original order. Write a table with no matches, all matches and a mixed collection. Keep the source array unchanged and include zero-length input.

Then add one rejected record shape missing remote. Optional extension: mount a filter control and assert the rendered result changes. The basic exercise focuses on an independent expected id list, not on copying the filter expression into the test.

Step 1: Specify the expected remote id list manually. Step 2: Check order and source nonmutation.

Practice starter: solve before reading the solution

typescript

type Role={id:string;remote:boolean};
function selectRemote(roles:readonly Role[]):Role[]{throw new Error("TODO");}
Reasoning
Use a hand-written expected id list rather than repeating the implementation predicate in the test.
  • Hint 1: Keep compiler-only rejected uses separate from runtime fixtures.
  • Hint 2: A copied predicate is a weak independent oracle.

Solution, tests and trade-offs

The solution uses a simple predicate and returns a new array.

The solution uses a simple predicate and returns a new array. The test oracle is a hand-written expected id sequence, so an incorrect predicate or reversed order will fail. The readonly input contract and runtime nonmutation assertion cover different aspects of ownership.

Run the compiler and runtime suite separately, then inspect any failure at the correct layer. A green type check with a red behavior test is not contradictory: types constrain possible uses, while tests evaluate the chosen implementation on specific examples.

Practice solution and acceptance checks

typescript

type Role={id:string;remote:boolean};
function selectRemote(roles:readonly Role[]):Role[]{return roles.filter(role=>role.remote);}

// Acceptance checks
const roles=[{id:"a",remote:true},{id:"b",remote:false},{id:"c",remote:true}];if(selectRemote(roles).map(x=>x.id).join(",")!=="a,c"||roles.length!==3||selectRemote([]).length!==0||selectRemote([{id:"x",remote:false}]).length!==0)throw new Error("remote");
Reasoning
The implementation preserves order and source ownership while the independent oracle checks behavior.

Interview reasoning and next step

In interviews, propose one test that catches an accidental any, one that catches a wrong algorithm and one that catches a broken UI action.

In interviews, propose one test that catches an accidental any, one that catches a wrong algorithm and one that catches a broken UI action. Explain why test scope should follow the risk rather than maximizing the number of nearly identical assertions.

The next lessons apply this discipline to compiler upgrades. Pin versions, compare diagnostics and declarations, and retain runtime tests instead of treating a faster compiler as evidence that application behavior is unchanged.

Optional stretch — after the core task works: Add a real filter-control interaction test.

Choose regression checks that fail for meaningful contract changes.