TypeScript TS2339: Property 'X' does not exist on type 'Y'
Property does not exist on type
Verified against TypeScript docs: Compiler Errors, TypeScript handbook: Narrowing, TypeScript 5.5 release notes · Updated May 2026
> quick_fix
TypeScript thinks the property doesn't exist on the value's type. Three causes: the type is wider than expected (a union missing a narrowing branch), the type definition is genuinely missing the property, or the value is typed 'unknown'/'never'. Add a type guard, fix the type, or assert after validation.
// Common case: union type, accessing a non-shared property
type Result = { ok: true; data: string } | { ok: false; error: string }
function handle(r: Result) {
// TS2339: 'data' does not exist on { ok: false; error: string }
console.log(r.data)
}
// Fix: narrow with discriminated union
function handle(r: Result) {
if (r.ok) console.log(r.data)
else console.log(r.error)
}What causes this error
TS2339 fires when property-access syntax (obj.prop or obj['prop']) targets a property the compiler can't find on the receiver's type. For unions, the property must exist on every member. For interfaces, the property must be declared. For unknown, no property access is allowed without narrowing. The compiler's apparentType + getPropertyOfType path determines what's available.
How to fix it
- 01
step 1
Read the receiver's type from the error
TS2339 names the type that lacks the property. Hover the variable in your IDE to confirm. The fix depends on whether the type is genuinely missing the property or is too wide.
Property 'role' does not exist on type 'User'. Type: { id: number; name: string } - 02
step 2
If it's a union type, narrow with a discriminator
Most common cause: the variable is a discriminated union and you accessed a property only some branches have. Narrow with the discriminator field first.
type Event = | { kind: 'click'; x: number; y: number } | { kind: 'key'; key: string } function handle(e: Event) { // TS2339: 'x' does not exist on type 'Event' console.log(e.x) } // Fix function handle(e: Event) { if (e.kind === 'click') { console.log(e.x, e.y) // narrowed to click branch } } - 03
step 3
If the type is genuinely missing the property, update it
If you control the type definition and the property is real, add it. If it's an external library type, augment the module declaration or check for a newer @types version.
// Update your interface interface User { id: number name: string role: 'admin' | 'user' // add it } // Or augment external types in a .d.ts declare module 'react' { interface CSSProperties { '--my-custom-var'?: string } } - 04
step 4
For unknown, validate before access
If the variable is typed 'unknown' (e.g., from JSON.parse), TypeScript blocks all property access until you validate. Use a type predicate or schema validation.
function isUser(x: unknown): x is User { return typeof x === 'object' && x !== null && 'id' in x && 'name' in x } const data: unknown = JSON.parse(raw) if (isUser(data)) { console.log(data.name) // OK after narrowing } - 05
step 5
For Record types, use bracket notation cautiously
obj['key'] on a Record<string, unknown> returns unknown. Prefer typed access via Pick or Omit, or use 'in' check before access.
function getValue(obj: Record<string, unknown>, key: string) { if (key in obj) { return obj[key] // unknown, but accessible } return undefined } - 06
step 6
Check for 'never' types
If the receiver is 'never', no property exists. This often means a discriminated-union check has narrowed too far - usually a switch with all cases handled. Add a default branch or check the narrowing logic.
function exhaust(x: 'a' | 'b'): string { if (x === 'a') return 'A' if (x === 'b') return 'B' // x is 'never' here - if you access x.something, TS2339 throw new Error('unreachable') }
Why TS2339 happens at the runtime level
TypeScript resolves property access via getPropertyOfType in checker.ts. For object types it looks up the property in the type's symbol table; for unions, every member must contain the property; for intersections, any member providing the property is sufficient; for primitives, the apparent type (the prototype's type, e.g., string for string literals) is consulted. When the property is not found in any of these paths, the compiler emits TS2339 with the receiver type's name. Discriminated unions add complexity: the compiler narrows the type at each control-flow point, so a property accessed before narrowing fails while the same access after narrowing succeeds.
Common debug mistakes for TS2339
- Accessing a non-shared property on a union type without first narrowing with a discriminator.
- Treating a JSON.parse result as a typed value without runtime validation; the type is 'any' and TS doesn't catch typos.
- Using 'as' to bypass TS2339 without checking that the property actually exists at runtime.
- Adding a custom CSS variable without augmenting CSSProperties; React's types reject it as TS2339.
- Mixing 'unknown' (which blocks all access) with 'any' (which allows any access) inconsistently.
When TS2339 signals a deeper problem
Frequent TS2339 errors signal that the codebase relies on dynamic property access without typed contracts. The architectural fix is to model unions as discriminated unions (with a literal-string 'kind' field), validate external data with zod or io-ts at every boundary, and prefer narrow types over Record<string, unknown> when possible. Module augmentation can fix gaps in third-party types, but the bigger signal is whether the codebase trusts types as contracts or treats them as suggestions. Code that uses 'any' liberally will see fewer TS2339 errors but more runtime crashes; code with strict types will see more compile errors but fewer surprises in production.
Frequently asked questions
Why does TS2339 fire on a property I can clearly see in the runtime data?
TypeScript only sees the type, not the runtime data. If the type definition is incomplete or the type was inferred too narrowly, the compiler doesn't know about properties that exist at runtime. This is the 'JSON.parse returns any' problem dressed up. The fix is to align types with reality: write a type that matches the actual data, validate at runtime with zod or a type predicate, then the compiler knows the property exists. Adding 'as any' or 'as Foo' silences the error but loses safety; the runtime crash will be just as confusing.
How is TS2339 different from TS2551?
TS2339 ('Property does not exist') fires when the property is genuinely absent. TS2551 ('Property X does not exist on type Y. Did you mean Z?') fires when the compiler thinks you typoed - it suggests a similar-named property that does exist. TS2551 is just TS2339 with a hint. The fix is the same: either correct the typo, fix the type definition, or narrow the union. Hover the variable to confirm what the compiler thinks the type is, then act on the actual gap.
Why does my third-party library show TS2339 for a documented property?
Three causes: (1) the library's TypeScript types lag behind its JS implementation; check for a newer @types/library version or contribute the missing property. (2) The property is added by a plugin you're using; declare it via module augmentation. (3) The library publishes types that are deliberately strict, and the property is only available in a specific config (e.g., Next.js Page-only props vs App Router). Read the type source in node_modules to see what's actually declared, then augment if necessary.
Should I use 'in' or optional chaining?
Different purposes. 'in' is for narrowing TypeScript's view of an object: 'if ("prop" in obj)' tells the compiler the property exists. Optional chaining 'obj?.prop' is for runtime safety against null/undefined. They compose: if (obj && 'prop' in obj) obj.prop. For TS2339 specifically, 'in' is what you want when narrowing a union; optional chaining doesn't change the type, it just survives a missing receiver. Use the right tool for which problem the compiler is reporting.