The Error
You access a property, and TypeScript immediately underlines it in red:
const user = {};
console.log(user.name); // Property 'name' does not exist on type '{}'. ts(2339)
Same thing happens with function returns, API responses, or React state:
function getUser() {
return {};
}
const user = getUser();
console.log(user.name); // ts(2339)
TypeScript inferred the type as {} โ an empty object with no properties. It won't compile because it has no evidence that name exists.
Root Cause
TypeScript is structurally typed. A type contains only the properties you explicitly declare โ nothing is assumed. Write {} or return a bare object with no annotation, and TypeScript picks the narrowest type it can infer. Any property access on that type triggers ts(2339).
The most common triggers:
- Object initialized as
{}then populated later - Function typed to return a generic object (
object,{},Record<string, unknown>) - API/JSON response typed as
anynarrowed to{}somewhere - Missing or incorrect interface definition
- Property added conditionally but not reflected in the type
Fix 1: Define an Interface or Type
Start here. Declare the shape of your object explicitly โ this is the fix that scales:
interface User {
name: string;
age?: number; // optional
}
const user: User = { name: 'Alice' };
console.log(user.name); // OK
For function returns, annotate the return type too:
function getUser(): User {
return { name: 'Alice' };
}
const user = getUser();
console.log(user.name); // OK
Fix 2: Type Assertion
You know the data shape, but TypeScript doesn't. Use as to tell it:
const raw = JSON.parse(response) as { name: string };
console.log(raw.name); // OK
Or when building an object incrementally:
const user = {} as User;
user.name = 'Alice';
console.log(user.name); // OK
Caution: Assertions bypass type-checking entirely. If the actual data doesn't match, you get a runtime error with no warning. Only reach for this when you're certain about the shape.
Fix 3: Use a Type with an Index Signature
Dynamic keys are a different problem. If you can't know property names at compile time, an index signature is the answer:
const user: Record<string, unknown> = { name: 'Alice' };
console.log(user['name']); // OK
// With a more specific value type:
const config: Record<string, string> = {};
config.theme = 'dark'; // OK
Fix 4: Narrow the Type with a Type Guard
API responses often arrive typed as unknown. A type guard verifies the shape before you access anything:
function hasName(obj: unknown): obj is { name: string } {
return typeof obj === 'object' && obj !== null && 'name' in obj;
}
const data: unknown = JSON.parse(response);
if (hasName(data)) {
console.log(data.name); // TypeScript knows this is safe
}
Fix 5: Optional Chaining (for Genuinely Optional Properties)
Sometimes a property may or may not exist โ and that's intentional. Declare it optional in the type, then handle the absent case at the call site:
interface User {
name?: string;
}
const user: User = {};
console.log(user.name ?? 'Anonymous'); // OK
One thing to know: optional chaining alone doesn't fix ts(2339). The property still has to appear in the type definition, even if marked optional.
Fix 6: Extend an Existing Type
Library types rarely cover your custom fields. Extend them rather than redefining from scratch:
// Express Request with a custom auth field
import { Request } from 'express';
interface AuthRequest extends Request {
user?: { name: string };
}
app.get('/', (req: AuthRequest, res) => {
console.log(req.user?.name); // OK
});
Fix 7: Use Generics for Reusable Functions
Generics solve the reusability problem. Constrain the type parameter to require the property you need:
function getName<T extends { name: string }>(obj: T): string {
return obj.name; // OK โ T is guaranteed to have 'name'
}
getName({ name: 'Alice', age: 30 }); // works
getName({}); // ts(2345) โ caught at the call site, not buried inside the function
Verify the Fix
Run the compiler with --noEmit to check your whole project without producing output files:
npx tsc --noEmit
Clean output means ts(2339) is gone. In VS Code, the red underline vanishes as soon as you save. Hover over the variable โ the tooltip should show your interface name, not {}.
Prevention
- Enable strict mode in
tsconfig.json:"strict": true. Catches ts(2339) and related errors early, before they cascade into bigger problems. - Avoid
anyfor API responses. Useunknownand narrow with type guards โ this keeps the type system honest. - Write types before implementation. Interfaces are quick to define and save significant debugging time later.
- Use the
satisfiesoperator (TypeScript 4.9+) to validate an object matches a type without widening it:
const user = { name: 'Alice', age: 30 } satisfies User;
console.log(user.name); // OK โ type stays narrow, not widened to User

