TypeScript 3.9 beta checks whether a suppressed error still exists
ts-expect-error reports an unused directive when the following line stops producing a diagnostic. That gives negative type tests and temporary workarounds an explicit expiry condition.
A suppression comment can survive long after the error that justified it disappears. @ts-ignore does not object when the following line becomes valid. The TypeScript 3.9 beta announced March 27 adds @ts-expect-error, which reports a diagnostic when its suppression is unused. The comment can now assert that a line is supposed to fail checking.
This is useful for a library whose types deliberately reject some calls while its runtime also validates JavaScript input. A test may need to make an invalid call to check that runtime validation. The compiler should recognise the intentional error without allowing the test to become silently valid after a declaration changes.
I value TypeScript's ability to make framework and library contracts visible during editing. A declaration also needs evidence that it rejects the inputs it is meant to reject. This feature gives that part of a library's behaviour a direct expression in checked source.
An invalid call can belong in a valid test
The beta announcement uses a function that accepts strings and asserts their types at runtime. A test passes numbers and expects an exception. Without a suppression, the test source fails compilation before it can exercise the runtime check.
The following example separates the invalid call from the runtime assertion. Its implementation deliberately checks a condition that JavaScript consumers can violate even though TypeScript callers see a string parameter.
function requireName(value: string): string {
if (typeof value !== "string") {
throw new TypeError("name must be a string");
}
return value;
}
let rejected = false;
try {
// @ts-expect-error
requireName(42);
} catch (error) {
rejected = error instanceof TypeError;
}
console.assert(rejected);
The directive allows that intentional type error on the next line. It does not implement the runtime assertion. The function and test still have to establish that the actual JavaScript operation throws. Compile-time rejection and runtime validation remain separate properties.
If the parameter changes to accept a number, the call no longer produces the expected type error. The directive then becomes unused and the compiler reports it. That is the additional check an ordinary ignore comment would not supply.
The implementation tracks use of a directive
Josh Goldberg's implementation pull request explains the mechanism. The scanner records comment directives, and type checking maps them by line number. When a diagnostic is suppressed by a preceding directive, that directive is marked as used. An expectation directive left unused produces a new diagnostic.
This mechanism defines the feature's limit. The comment expects an error, but it does not name a particular diagnostic code or message. Another error on the line can keep the directive used. A narrow test statement makes its intent easier to assess because fewer unrelated operations can produce the diagnostic being suppressed.
The compiler's new diagnostic identifies an unused @ts-expect-error directive. That gives a temporary workaround a detectable expiry condition. It does not automatically repair the code or decide whether the changed declaration is correct. The maintainer still interprets why the expected failure disappeared.
An ignore comment can still express a different intention
The announcement does not recommend replacing every ignore comment indiscriminately. It describes an upgrade scenario where a statement errors under one TypeScript version and succeeds under another. A checked expectation can make the successful version fail because its directive is unused.
That is a different requirement from a negative type test. A compatibility workaround may intentionally permit either compiler result during a transition. A negative test intends to require rejection. Choosing the comment therefore depends on whether absence of a diagnostic should itself fail the check.
I prefer the expectation form when the rejection is part of the contract under test. It records a property that can change, instead of permanently removing a line from diagnostic reporting. Cross-version compatibility still needs a deliberate rule for which compiler outcomes are acceptable.
The beta also concentrates on compiler performance. Its announcement identifies expensive cases involving unions, intersections, conditional types and mapped types, with improvements motivated by libraries including material-ui and styled-components. Those changes target the cost of checking complex declarations. They do not alter the basic reason to keep negative tests for those declarations.
The announcement also separates the shipped inference repairs from the proposed awaited operator. That operator has been removed from the release after early testing showed that it needed more design work. An expectation test written for this beta should describe behaviour the beta actually implements. A plan for another type operation cannot establish the compiler result that the test expects today.
The expected failure has to remain specific
A useful type test places the operation whose rejection matters directly after the directive. The runtime test can then check the corresponding behaviour independently. Combining many operations on that line would make it harder to know which diagnostic satisfied the expectation.
The result is a more informative failure when a declaration changes. A formerly rejected call becoming accepted now makes its expectation directive unused. The source identifies the assertion that needs review, while ordinary runtime assertions continue to establish what the emitted JavaScript does with the invalid input.