Quick answer
You accessed a property the value's type doesn't declare. Give the value a proper type/interface (especially for JSON.parse results and {}), narrow a union before accessing a member-specific property, or fix a typo. Avoid (obj as any).prop — it just hides the problem.
The exact error string
const obj = {};
obj.name;
// error TS2339: Property 'name' does not exist on type '{}'.
const data = JSON.parse(text); // type: any (or {} if annotated)
data.user.name; // unchecked on any; TS2339 once typed
How to diagnose it (read the type it names)
The fix is decided by the type after "on type". Hover the value in your editor and read what TypeScript thinks it is — that tells you which case you're in:
{}/object/unknown→ the value is untyped — give it an interface (Fix 1).- a union (
A | B) → the property is on only one member — narrow first (Fix 2). - the right type, but the property is misspelled → fix the name (check casing).
never→ an earlier guard eliminated every member — the narrowing is wrong.
Fix 1: give the value a real type
interface User { id: number; name: string; }
const user: User = JSON.parse(text);
user.name; // ✅ known property
const map: Record<string, number> = {};
map["count"] = 1; // ✅ index signature allows string keys
Fix 2: narrow a union before access
type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
function area(s: Shape) {
s.r; // ❌ not on every member
if (s.kind === "circle") return Math.PI * s.r ** 2; // ✅ narrowed
}
Fix 3: augment a global or library type
declare global {
interface Window { dataLayer: unknown[]; }
}
window.dataLayer.push({}); // ✅ now known
Worked example: untyped API JSON
The most common real trigger on a JSON app: response.json() returns any, so the moment you give it a real type (which you should), TS2339 flags the field that isn't actually there or is nested differently:
interface User { id: number; profile: { name: string } }
const user: User = await (await fetch("/api/user")).json();
user.name;
// error TS2339: Property 'name' does not exist on type 'User'.
// -> it's nested: user.profile.name
Generate the interface from a real response with JSON to TypeScript so the property names and nesting match the data — then TS2339 only fires on genuine typos.
TS2339 in React
// 1) event.target is the generic EventTarget — type the handler:
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => e.target.value; // ✅
// 2) accessing a prop not declared in Props:
function Card({ title }: { title: string }) {
return <h3>{title} {subtitle}</h3>; // ❌ 'subtitle' not on props — add it to the type
}
// 3) useRef element access:
const ref = useRef<HTMLInputElement>(null);
ref.current?.focus(); // ✅ typed element, optional-chained
Common variants of this message
| Variant | Typical cause | Fix |
|---|---|---|
... does not exist on type '{}' | untyped / empty object | declare an interface |
... does not exist on type 'never' | over-narrowed union | fix the type guard |
... does not exist on type 'EventTarget' | DOM event target | cast or type the handler |
... does not exist on type 'A | B' | union member-only prop | narrow with in/typeof/discriminant |
... does not exist on type 'Window' | custom global | declare global augmentation |
Debugging checklist
- ✓ Hover the value — read the type after "on type" to pick the fix
- ✓
{}/object/any? Give it an interface - ✓ Typo? Check the exact property name and casing
- ✓ Union? Narrow with
in/typeof/a discriminant first - ✓ DOM event? Type the handler (
React.ChangeEvent<...>) or caste.target - ✓ Global?
declare global { interface Window { ... } } - ✓ API JSON? Generate the interface with JSON to TypeScript — avoid
as any
Frequently Asked Questions
Why does it work in plain JavaScript but error in TypeScript?
JavaScript lets you read any property and returns undefined if it doesn't exist; TypeScript refuses unless the property is declared on the type. TS2339 is catching a likely bug — a typo or a wrong assumption about the shape — before it becomes an undefined at runtime.
What does 'Property does not exist on type never' mean?
The value was narrowed to never — every member of its union was eliminated by an earlier check, or a filter/guard left nothing. It usually means a type guard is wrong or too aggressive. Re-check the narrowing logic so the value keeps the member that actually has the property.
How do I type event.target.value in a DOM or React handler?
event.target is typed as the generic EventTarget, which has no value. In the DOM, cast it: (e.target as HTMLInputElement).value. In React, type the handler instead: (e: React.ChangeEvent<HTMLInputElement>) => e.target.value, which is checked without a cast.
Why does TS2339 appear after Object.keys or a .filter?
Object.keys returns string[], not the object's key type, and some array methods widen or narrow the element type in ways you didn't intend. Cast the key to keyof typeof obj, or annotate the result, so the property access is on the specific type again.
Should I add the property to the interface or mark it optional?
If the property is always present, add it. If it is only sometimes there (an optional API field), add it with a ? so consumers must handle the undefined case. Do not add fields that the data does not actually return — that just moves the bug downstream.
Is a window/globalThis property fixed by casting?
Casting (window as any).foo works but kills type safety. Instead augment the type once: declare global { interface Window { foo: string } }. Then window.foo is known everywhere and still type-checked.
Generate accurate types from your JSON
Paste a real API response and get a TypeScript interface with every property typed — nothing is uploaded to a server.