Should I test that?

Should I test what static analysis already checks?

Verdict

No

Do not write tests that repeat a rule your type checker or linter enforces on every merge, such as argument types or a missing null check; spend the tests on the values the code computes.

Why

Do not write tests that repeat a rule your type checker or linter enforces on every merge. For the typical case, tsc, mypy or ESLint running in CI as a blocking step, Blast radius is users, Change frequency is regularly, Detectability is immediately, Reversibility is trivial, and Test cost is trivial. Detectability is immediately because a wrong argument type or an unchecked null fails the author's build, and Reversibility is trivial because code that never merges stores nothing. Rule R8 gives Do not test; once the check stops blocking the merge, Detectability moves to same-day and the decision becomes Test.

When the decision changes
WhenDecisionWhy
The code gives the type checker the shape of a JSON response through a cast, such as `as Profile`Test: parse the response against a schema, and test that the loader rejects a renamed fieldDetectability moves to eventually and Reversibility to with-effort: the checker trusts the cast, and users save records with the missing field
The rule does not block the merge: it is a dashboard warning, strict mode is off, or the line carries `eslint-disable` or `# type: ignore`Test the paths the rule would flag, such as a missing value, until the rule blocks the mergeDetectability moves to same-day: a null dereference reaches users as a crash
The checked function takes an amount, and the type checker sees cents and euros as the same `number`Test mandatory: one test with a known amount for each unit conversionBlast radius rises to money, Detectability moves to eventually and Reversibility to costly: an amount in the wrong unit passes every type check
The checked code filters records by the user's account, and a security scanner such as CodeQL covers the fileTest mandatory: one allowed and one denied request from a user of another accountBlast radius rises to safety-or-legal and Detectability moves to never: the scanner matches injection patterns, not a filter on the wrong column
The team wrote its own lint rule, such as a ban on `fetch` outside the API clientTest the rule with valid and invalid code samples in ESLint's RuleTesterBlast radius falls to internal, Detectability moves to eventually and Reversibility to with-effort: a broken rule stops reporting silently

What breaks if you don't test

In the typical case nothing breaks that CI misses: a call with the wrong type never reaches the main branch. Failures come from code the checker only appears to cover. A cast such as as Profile tells TypeScript to trust a JSON response, so a renamed API field arrives as undefined while the build stays green. An eslint-disable comment switches a rule off for one line that nobody rereads.

What you lose if you over-test

A test that calls getUser("42") under @ts-expect-error to prove that a string ID is rejected fails only when someone changes the signature on purpose, and then each change touches two files. These tests also inflate coverage: the lines run, the report goes green, and the calculation inside the function has no assertion on its result.

What to do instead

Make the check a gate: run tsc --noEmit or mypy --strict in CI, and ESLint with --max-warnings 0 so that a warning fails the build. Where input enters the program (JSON responses, environment variables, message payloads), replace casts with a schema parse and test that it rejects malformed input; the TypeScript handbook states that a type assertion has no runtime check. Spend unit tests on computed values and branches, which no checker sees.

When the answer changes

  • The code gives the checker a type through a cast, any, or a suppression comment.
  • The rule is a warning that does not fail CI, or strict mode is off for the file.
  • The value carries money, dates, or permissions, which a type cannot validate.

Code example + Counterexample

The cast that unsubscribed users

This loader compiles under strict and passes ESLint:

type Profile = { id: string; weeklyDigest: boolean };

async function loadProfile(id: string): Promise<Profile> {
  const res = await fetch(`/api/profiles/${id}`);
  return (await res.json()) as Profile; // tsc checks nothing here
}

test("loadProfile rejects a response without weeklyDigest", async () => {
  vi.stubGlobal("fetch", async () =>
    Response.json({ id: "u1", weekly_digest: true }),
  );
  await expect(loadProfile("u1")).rejects.toThrow();
});

Suppose the API renames weeklyDigest to weekly_digest. The settings form then shows the box unchecked, and every user who saves the form unsubscribes without knowing it. "The type checker passed, so no test" is wrong for loadProfile: Detectability is eventually, because nobody reports an email that stopped arriving, and Reversibility is with effort, because a script must restore the setting from the audit log, so the framework returns Test. The test above fails against the cast and passes once loadProfile returns ProfileSchema.parse(await res.json()).

FAQ

Should I add unit tests for things already checked by a static analysis tool?

No, a rule that your static analysis tool enforces on every merge needs no unit test that repeats it. Write one when a cast or a suppression comment switches the check off, or when the value carries money or access.

Is testing with linters enough?

No, a linter checks the form of code and a type checker checks shapes; neither checks returned values. A date function returning the UTC day instead of the user's local day passes both. Write a test that asserts the returned day.

Should the no-magic-numbers lint rule apply to test code?

No, turn off no-magic-numbers for test files, because a literal expected value such as 1999 keeps an assertion independent of the code under test. A test that imports the same constant as the code it checks still passes when that constant is wrong.

Do I need unit tests if I use TypeScript?

Yes, TypeScript replaces only the tests that check types; it does not check values, and a type assertion has no check at run time. Keep unit tests for calculations, branches and the parsing of external input.