DedSec Project

TS2339 — DedSec Project

TS2339 — narrow a union before reading a variant-specific property Root cause TypeScript TS2339 reports that a property does not exist on the static type being accessed. With union types…

TS2339 — narrow a union before reading a variant-specific property

Root cause

TypeScript TS2339 reports that a property does not exist on the static type being accessed. With union types, some variants may have the property while others do not. For example, a touch event contains x/y coordinates while a typing event contains text. Reading event.x before inspecting the variant would be invalid for a typing event.

Reliable fix

Use a discriminated union with a literal kind field. Checking event.kind === 'tap' narrows the type to the branch containing x and y; the alternative branch uses its text field. The example verifies both at runtime and remains fully checked under --strict.

Verified TypeScript example

type UiEvent = {kind: 'tap'; x: number; y: number} | {kind: 'type'; text: string};
function render(event: UiEvent): string {
  if (event.kind === 'tap') return `tap at ${event.x},${event.y}`;
  return `typed ${event.text}`;
}
if (render({kind: 'tap', x: 2, y: 3}) !== 'tap at 2,3') throw Error();
if (render({kind: 'type', text: 'ok'}) !== 'typed ok') throw Error();
console.log('PASS TS2339');

Why it works

A property added to a JavaScript object does not modify its declared compile-time type without updating that type or using an appropriate guard. Optional chaining is not a substitute when a property is absent from every member of the static type. An unsafe cast to any hides the diagnostic and can make later code crash.

Reproduction and build commands

Save as main.ts and run tsc --strict --target es2022 --module commonjs --outDir dist main.ts && node dist/main.js. Expected PASS. To reproduce the compiler warning in a separate bad.ts, declare the same UiEvent type and write function wrong(e: UiEvent) { return e.x; }; expect TS2339.

Diagnostics to practice

Add a 'scroll' variant and handle it by an exhaustive switch with a never assertion in the default branch. Try a typo such as event.txt, and compare compiler feedback with an untyped dynamic object. Explain why the discriminant is more reliable than guessing fields based on truthiness.

Reference: https://www.typescriptlang.org/docs/handbook/2/narrowing.html