Assert that an invalid call is rejected at compile time — and stays rejected
createUser must never accept a user without an email. A runtime test cannot show this: the invalid call does not compile, so there is nothing for the runner to execute.
Requirements: assert in a test file that the call below is a compile error, and make the assertion itself fail the build on the day the call becomes legal, so the guarantee cannot rot unnoticed. Assume the file is inside the tsconfig that CI type-checks.
interface NewUser { name: string; email: string }
declare function createUser(u: NewUser): void;
// your code here
createUser({ name: 'Ada' });
Write the implementation.
Put @ts-expect-error above the invalid call. It compiles only while that line still errors, and becomes an error itself the day the call is accepted — so the assertion cannot rot, unlike @ts-ignore, which stays quiet forever. The file must sit inside the type-check run.
- ✗Trying to assert a compile error with a runtime
toThrow— the code never compiles, so nothing runs - ✗Using
@ts-ignore, which keeps passing after the error disappears and rots into a dead directive - ✗Leaving the type-test file out of the
tsconfigCI checks, so the assertion is never evaluated
- →Why does
@ts-expect-errorfail the build once the error underneath it is fixed? - →How would you assert an exact inferred return type rather than a rejected call?
The solution
import { expectTypeOf } from 'expect-type';
interface NewUser { name: string; email: string }
declare function createUser(u: NewUser): void;
// @ts-expect-error — email is required; if the call ever becomes legal, this line fails
createUser({ name: 'Ada' });
// and the precise check on the parameter's shape
expectTypeOf(createUser).parameter(0).toEqualTypeOf<NewUser>();
Why it works
@ts-expect-error is not merely a silencer. It requires that the next line have an error. While createUser({ name: 'Ada' }) fails to type-check, the file builds. On the day someone makes email optional, the error disappears — and the compiler reports Unused '@ts-expect-error' directive. The build fails, and the lost guarantee is noticed immediately.
@ts-ignore cannot do this: it stays quiet whether the error is there or not, so the check quietly decays into a dead comment.
⚠️ All of this works only if the file is part of tsc --noEmit in CI. A runner that merely transpiles the tests (esbuild, SWC) never checks types — and the assertion would never fire.