Skip to content

Fix: TypeScript Property does not exist on type (TS2339)

FixDevs ·

Quick Answer

How to fix TypeScript error TS2339 'Property does not exist on type'. Covers missing interface properties, type narrowing, optional chaining, intersection types, index signatures, type assertions, type guards, window augmentation, and discriminated unions.

The Error

You access a property on an object and TypeScript throws:

error TS2339: Property 'foo' does not exist on type 'Bar'.

Or one of its common variants:

error TS2339: Property 'name' does not exist on type '{}'.
error TS2339: Property 'dataset' does not exist on type 'Element'.
error TS2339: Property 'myGlobal' does not exist on type 'Window & typeof globalThis'.
error TS2339: Property 'value' does not exist on type 'EventTarget'.

TS2339 means you’re trying to use a property that TypeScript doesn’t see in the type definition. The property might actually exist at runtime, but TypeScript’s static analysis doesn’t know about it. The fix depends on why TypeScript can’t find the property.

Why This Happens

TypeScript uses structural typing. Every variable has a type, and that type defines which properties are available. When you access a property that isn’t part of the type definition, TypeScript blocks it.

This happens for several reasons:

  1. The property genuinely doesn’t exist on the type, and you have a typo or are using the wrong object.
  2. The type is too narrow. You defined an interface but forgot to include the property.
  3. The type is too broad. You’re working with {}, object, Element, or EventTarget instead of a more specific type.
  4. The property is conditional. It exists on some variants of a union type but not others, and TypeScript needs you to narrow the type first.
  5. You’re augmenting a global object like window or process.env without declaring the extra properties.

Understanding which case you’re dealing with determines the right fix. Let’s go through each one.

Fix 1: Add the Missing Property to Your Interface or Type

The most common cause is that your type definition simply doesn’t include the property you’re accessing.

interface User {
  name: string;
  email: string;
}

const user: User = { name: "Alice", email: "[email protected]" };
console.log(user.age); // Error: Property 'age' does not exist on type 'User'

The fix is straightforward. Add the property to the interface:

interface User {
  name: string;
  email: string;
  age: number;
}

If the property is not always present, make it optional:

interface User {
  name: string;
  email: string;
  age?: number;
}

With an optional property, TypeScript knows user.age might be undefined. You’ll need to handle that, which is covered in our guide on fixing “Object is possibly undefined”.

Pro Tip: Before adding properties, double-check for typos. A surprising number of TS2339 errors come from misspelling a property name. If you wrote user.naem instead of user.name, TypeScript correctly tells you the property doesn’t exist. Fix the typo, not the type.

Fix 2: Use Type Narrowing for Union Types

When a variable can be one of several types, TypeScript only allows you to access properties that exist on all types in the union.

interface Dog {
  breed: string;
  bark(): void;
}

interface Cat {
  breed: string;
  purr(): void;
}

type Pet = Dog | Cat;

function handlePet(pet: Pet) {
  pet.bark(); // Error: Property 'bark' does not exist on type 'Pet'
              // Property 'bark' does not exist on type 'Cat'
}

You can access breed because both Dog and Cat have it. But bark only exists on Dog. TypeScript needs you to narrow the type first.

Use an in check:

function handlePet(pet: Pet) {
  if ("bark" in pet) {
    pet.bark(); // TypeScript now knows this is Dog
  } else {
    pet.purr(); // TypeScript now knows this is Cat
  }
}

Or use a custom type guard:

function isDog(pet: Pet): pet is Dog {
  return "bark" in pet;
}

function handlePet(pet: Pet) {
  if (isDog(pet)) {
    pet.bark(); // Works
  }
}

Type narrowing is one of the most important TypeScript concepts to master. If you’re dealing with values that could be undefined after narrowing, see our guide on handling possibly undefined objects.

Fix 3: Use Discriminated Unions

A cleaner approach for complex union types is discriminated unions. Add a shared literal property that TypeScript can use to distinguish between types:

interface Dog {
  kind: "dog";
  breed: string;
  bark(): void;
}

interface Cat {
  kind: "cat";
  breed: string;
  purr(): void;
}

type Pet = Dog | Cat;

function handlePet(pet: Pet) {
  switch (pet.kind) {
    case "dog":
      pet.bark(); // Works — TypeScript narrows to Dog
      break;
    case "cat":
      pet.purr(); // Works — TypeScript narrows to Cat
      break;
  }
}

The kind property is the discriminant. TypeScript uses its literal value to narrow the type inside each branch. This pattern scales well when you have many variants and is easier to maintain than in checks.

Fix 4: Use Intersection Types to Extend Existing Types

Sometimes you need to add properties to a type you don’t control. Intersection types let you combine types:

interface BaseUser {
  name: string;
  email: string;
}

type AdminUser = BaseUser & {
  role: "admin";
  permissions: string[];
};

const admin: AdminUser = {
  name: "Alice",
  email: "[email protected]",
  role: "admin",
  permissions: ["read", "write", "delete"],
};

admin.permissions; // Works

This is different from interface extends, which also works:

interface AdminUser extends BaseUser {
  role: "admin";
  permissions: string[];
}

Both approaches add the missing properties. Use extends when you’re building a hierarchy. Use intersections (&) when you’re composing types on the fly.

If TypeScript then complains that a value doesn’t match the extended type, check our guide on fixing “Type is not assignable to type”.

Fix 5: Add an Index Signature for Dynamic Properties

When an object has dynamic keys that you don’t know at compile time, TypeScript can’t verify individual property names. An index signature tells TypeScript the shape of unknown properties:

interface Config {
  appName: string;
  [key: string]: string; // Index signature
}

const config: Config = {
  appName: "MyApp",
  version: "1.0.0",
  env: "production",
};

console.log(config.version); // Works
console.log(config.anything); // Works — TypeScript allows any string key

Common Mistake: Index signatures turn off type checking for unknown properties. You lose the safety net. If you know the possible keys, prefer a mapped type or Record:

type AllowedKeys = "version" | "env" | "debug";

type Config = {
  appName: string;
} & Record<AllowedKeys, string>;

Or use a Map instead when the keys are truly dynamic:

const config = new Map<string, string>();
config.set("version", "1.0.0");
config.get("version"); // string | undefined

With Map.get(), the return type is string | undefined, so you get the safety of knowing the key might not exist.

Fix 6: Use Type Assertions (With Caution)

A type assertion tells TypeScript to treat a value as a specific type. It silences TS2339 but doesn’t add runtime safety:

const input = document.getElementById("search") as HTMLInputElement;
input.value; // Works — TypeScript treats it as HTMLInputElement

Without the assertion, getElementById returns HTMLElement | null. The value property exists on HTMLInputElement but not HTMLElement, so TypeScript blocks it.

A safer approach combines a null check with a type guard:

const input = document.getElementById("search");
if (input instanceof HTMLInputElement) {
  input.value; // Works — TypeScript narrows to HTMLInputElement
}

Warning: Avoid using as any to silence TS2339. It hides real bugs. If you find yourself reaching for as any, step back and ask which of the other fixes in this article actually solves the problem.

The as syntax only works in .ts files. If you’re in a .js file with JSDoc types and TypeScript can’t find a module at all, see our guide on fixing “Cannot find module”.

Fix 7: Use Type Guards Instead of Assertions

Type guards are runtime checks that TypeScript understands. Unlike assertions, they actually verify the type at runtime:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function processValue(value: unknown) {
  if (isString(value)) {
    value.toUpperCase(); // Works — TypeScript narrowed to string
  }
}

Built-in type guards you should know:

  • typeof for primitives: typeof x === "string", typeof x === "number"
  • instanceof for classes: x instanceof Date, x instanceof HTMLInputElement
  • in for property existence: "name" in obj
  • Truthiness checks: if (x) narrows out null, undefined, 0, ""

Here’s a practical example with API responses:

interface SuccessResponse {
  status: "success";
  data: unknown;
}

interface ErrorResponse {
  status: "error";
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === "error") {
    console.error(response.message); // Works — narrowed to ErrorResponse
    return;
  }
  console.log(response.data); // Works — narrowed to SuccessResponse
}

Fix 8: Augment Window, Global, or Process Types

Accessing custom properties on window triggers TS2339 because TypeScript’s built-in Window type doesn’t include your custom properties:

console.log(window.myAnalytics); // Error: Property 'myAnalytics' does not exist on type 'Window'

Fix this with declaration merging. Create a .d.ts file (e.g., global.d.ts) in your project:

// global.d.ts
export {};

declare global {
  interface Window {
    myAnalytics: {
      track(event: string): void;
    };
  }
}

The export {} is required to make the file a module, which enables the declare global block.

For process.env in Node.js:

// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    API_KEY: string;
    NODE_ENV: "development" | "production" | "test";
  }
}

After adding these declarations, TypeScript recognizes the properties without errors.

Note: Make sure the .d.ts file is included in your tsconfig.json. If it’s outside your include paths, TypeScript won’t pick it up.

Fix 9: Handle the DOM’s EventTarget Problem

This is one of the most common TS2339 scenarios in frontend code:

document.addEventListener("click", (event) => {
  console.log(event.target.dataset.id);
  // Error: Property 'dataset' does not exist on type 'EventTarget'
});

event.target is typed as EventTarget | null, which doesn’t have dataset. You need to narrow it:

document.addEventListener("click", (event) => {
  const target = event.target;
  if (target instanceof HTMLElement) {
    console.log(target.dataset.id); // Works
  }
});

For form elements:

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const form = event.currentTarget as HTMLFormElement;
  const input = form.elements.namedItem("email") as HTMLInputElement;
  console.log(input.value);
});

If you’re working with React event handlers, the types are slightly different. React provides its own event types like React.ChangeEvent<HTMLInputElement>, which already narrow event.target to the correct element type.

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  console.log(event.target.value); // Works — already typed as HTMLInputElement
}

If event.target ends up being null or undefined at runtime, you’ll get a different error entirely. Our guide on fixing “Cannot read properties of undefined” covers that scenario.

Fix 10: Use Optional Chaining for Properties That May Not Exist

If a property might or might not exist on an object, optional chaining (?.) prevents the error at runtime while type narrowing handles the compile-time side:

interface Config {
  database?: {
    host: string;
    port: number;
  };
}

function getDbHost(config: Config): string {
  return config.database?.host ?? "localhost";
}

Optional chaining short-circuits to undefined if any part of the chain is null or undefined. Combined with the nullish coalescing operator (??), you get a clean default value pattern.

However, optional chaining doesn’t fix TS2339 on its own. If the property isn’t in the type definition at all, you still get the error. Optional chaining only helps when the property is defined as optional (?) in the type.

Fix 11: Handle Third-Party Library Types

Sometimes TS2339 appears because a library’s type definitions are outdated or incomplete:

import { someLib } from "some-library";
someLib.newMethod(); // Error: Property 'newMethod' does not exist

Several things could be going on:

  1. The @types package is outdated. Update it:
npm install @types/some-library@latest
  1. The library ships its own types but they’re wrong. You can augment them:
// some-library.d.ts
import "some-library";

declare module "some-library" {
  interface SomeLib {
    newMethod(): void;
  }
}
  1. There are no types at all. Create a declaration file:
// some-library.d.ts
declare module "some-library" {
  export function someLib(): void;
}

If TypeScript can’t find the module at all (not just a missing property), that’s a different error. See fixing “Cannot find module” for that case.

Fix 12: Handle JSON and API Response Types

Data from APIs or JSON files comes in as unknown or any. If you type it too broadly, TS2339 appears when you access specific properties:

async function fetchUser() {
  const response = await fetch("/api/user");
  const data = await response.json(); // Type is 'any'
  console.log(data.name); // Works with 'any', but no type safety
}

The right approach is to define the expected shape and validate it:

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(): Promise<User> {
  const response = await fetch("/api/user");
  const data: unknown = await response.json();

  // Runtime validation
  if (
    typeof data === "object" &&
    data !== null &&
    "name" in data &&
    "email" in data
  ) {
    return data as User;
  }
  throw new Error("Invalid user data");
}

For production code, use a validation library like Zod, Valibot, or io-ts. They generate TypeScript types from runtime schemas, so your types and validation stay in sync:

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(): Promise<User> {
  const response = await fetch("/api/user");
  const data = await response.json();
  return UserSchema.parse(data); // Throws if data doesn't match
}

Still Not Working?

If none of the fixes above solved your TS2339 error, try these:

Check your tsconfig.json strict mode. Some TS2339 errors only appear when strict: true is enabled. If you recently enabled strict mode, you may need to update types throughout your codebase. Don’t disable strict mode to dodge the errors — fix them properly.

Restart your TypeScript language server. In VS Code, press Ctrl+Shift+P (or Cmd+Shift+P on Mac) and run “TypeScript: Restart TS Server.” Stale caches sometimes show phantom errors that don’t actually exist.

Check for conflicting type definitions. If you have both @types/some-library and the library’s built-in types, they can conflict. Remove one:

npm uninstall @types/some-library

Check the TypeScript version. Some type narrowing features (like in operator narrowing) were improved in TypeScript 4.9+. If you’re on an older version, upgrade:

npm install typescript@latest

Look for type mismatches in generic functions. TS2339 sometimes appears deep inside generic code where the type parameter isn’t constrained properly:

// Broken — T could be anything
function getName<T>(obj: T) {
  return obj.name; // Error: Property 'name' does not exist on type 'T'
}

// Fixed — constrain T
function getName<T extends { name: string }>(obj: T) {
  return obj.name; // Works
}

Check for ESLint conflicts. Sometimes ESLint rules interfere with TypeScript’s type system, especially if your parser configuration is wrong. If you’re seeing parsing errors alongside TS2339, check our guide on fixing ESLint parsing errors.

Use the TypeScript Playground. If you can’t figure out why a type isn’t working, paste a minimal reproduction into the TypeScript Playground. It shows errors in real time and lets you experiment with different type structures without rebuilding your project.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles