【TypeScript】TS2339の原因と解決方法|Property does not exist on typeを完全解説

【TypeScript】TS2339の原因と解決方法|Property does not exist on typeを完全解説 TypeScript

TypeScriptを使っていると、必ず一度は遭遇するのがTS2339エラーです。「Property ‘X’ does not exist on type ‘Y’」というエラーメッセージが表示され、コンパイルが通らなくなります。

このエラーは「型YにプロパティXが存在しない」という意味で、TypeScriptの型安全性を守るための重要なチェックです。しかし、発生パターンが非常に多岐にわたるため、原因の特定と適切な修正方法を知らないと、開発の手が止まってしまいます。

この記事では、TS2339エラーが発生するすべてのパターンを網羅的に解説し、それぞれの原因正しい解決方法を具体的なコード付きで紹介します。基本的なケースから、ユニオン型の絞り込み、Object.keys()の罠、DOM操作、React/Vue、外部ライブラリまで、実務で遭遇するあらゆるシナリオをカバーします。

この記事で学べること

  • TS2339エラーの根本原因とエラーメッセージの読み方
  • 基本的なケース(タイポ・定義不足・プロパティ追加忘れ)の解決方法
  • ユニオン型・型の絞り込み(Type Guard)による解決パターン
  • Object.keys()・for…in・スプレッド演算子での発生と対処法
  • DOM操作(querySelector・HTMLElement)での型解決
  • クラスのprivate/protected・動的プロパティでの対処
  • 外部ライブラリ・JSON.parse・fetch/axiosでの型定義方法
  • React/Vue(state/props/ref/event)での解決パターン
  • 実務で頻出するTS2339パターンのベストプラクティス
スポンサーリンク
  1. TS2339エラーとは?
    1. エラーメッセージの読み方
    2. 具体例で理解する
    3. なぜこのエラーが存在するのか
    4. TS2339が発生する根本原因
  2. 基本的なケースと解決方法
    1. ケース1: 存在しないプロパティへのアクセス
    2. ケース2: タイポ(プロパティ名のスペルミス)
    3. ケース3: プロパティの追加忘れ
    4. ケース4: インターフェース定義の不足(型推論に頼りすぎ)
    5. ケース5: type と interface の使い分けミス
    6. ケース6: 関数の戻り値型が不足
  3. 型の絞り込み不足によるTS2339
    1. ユニオン型で共通でないプロパティにアクセス
    2. typeof による絞り込み
    3. instanceof による絞り込み
    4. in 演算子による絞り込み
    5. Discriminated Union(判別可能なユニオン)パターン
    6. カスタム型ガード関数
    7. nullチェックによる絞り込み
    8. 型の絞り込み方法の比較表
  4. オブジェクト操作でのTS2339
    1. Object.keys() の戻り値が string[]
    2. Object.entries() の型
    3. for…in ループでのプロパティアクセス
    4. スプレッド演算子での問題
    5. 動的プロパティアクセス(ブラケット記法)
    6. Record型を使ったインデックスアクセス
  5. 配列・DOM操作でのTS2339
    1. querySelector の戻り値が Element | null
    2. よく使うHTMLElement型の対応表
    3. HTMLElement 固有のプロパティ
    4. NodeList vs HTMLCollection
    5. イベントオブジェクトの型
    6. Array メソッドの戻り値型
  6. クラスでのTS2339
    1. private/protected プロパティへの外部アクセス
    2. 初期化されていないプロパティ
    3. 動的プロパティの追加
    4. 継承元にないプロパティ
  7. 外部ライブラリ・API連携でのTS2339
    1. JSON.parse() の戻り値が any
    2. fetch/axios のレスポンス型
    3. window オブジェクトのカスタムプロパティ
    4. グローバル変数の型定義
    5. @types パッケージの不足
    6. process.env の型
  8. React/Vue でのTS2339
    1. React: Props の型不足
    2. React: event.target のプロパティ
    3. React: Reactのイベント型一覧
    4. React: ref の型
    5. React: カスタムフックの戻り値
    6. Vue 3: Composition API でのTS2339
    7. Vue 3: テンプレートref の型
  9. 正しい解決アプローチ
    1. インターフェースの拡張(extends)
    2. 型ガードの活用
    3. インデックスシグネチャと Record 型
    4. Optional chaining(?.)との関係
    5. 型アサーション(as)はいつ使うべきか
    6. TS2339 解決方法の優先順位
  10. 実務でよくあるTS2339パターン10選
    1. パターン1: APIレスポンスの型定義不足
    2. パターン2: window.XXX(カスタムグローバル変数)
    3. パターン3: document.querySelector の型
    4. パターン4: Object.keys のイテレーション
    5. パターン5: JSON.parse の戻り値
    6. パターン6: イベントオブジェクトのプロパティ
    7. パターン7: ライブラリの型更新後
    8. パターン8: 環境変数 process.env
    9. パターン9: Map/Set の型
    10. パターン10: 条件分岐後の型絞り込みが効かない
  11. まとめ
    1. TS2339 vs TS2322 vs TS2345 の違い
    2. 関連記事

TS2339エラーとは?

TS2339は、TypeScriptコンパイラが出す型チェックエラーの1つです。あるオブジェクトの型にはないプロパティにアクセスしようとしたときに発生します。

エラーメッセージの読み方

TS2339エラーのメッセージは、常に以下の形式で表示されます。

error TS2339: Property ‘X’ does not exist on type ‘Y’.

このメッセージは次の2つの情報を伝えています。

要素 説明
X アクセスしようとしたプロパティ名 name, value, length
Y TypeScriptが認識しているそのオブジェクトの型 object, Element, {}

つまり、「型Yには、プロパティXが定義されていません」という意味です。TypeScriptは型情報に基づいてプロパティの存在をチェックするため、型にないプロパティにアクセスするとこのエラーが出ます。

具体例で理解する

最もシンプルな例を見てみましょう。

TS2339 の最もシンプルな例
const user = {
  name: '田中太郎',
  age: 30
};

// OK: 型に存在するプロパティ
console.log(user.name);  // "田中太郎"
console.log(user.age);   // 30

// TS2339: 型に存在しないプロパティ
console.log(user.email); // エラー!

error TS2339: Property ‘email’ does not exist on type ‘{ name: string; age: number; }’.

この例では、userオブジェクトの型は { name: string; age: number; } と推論されています。この型にはemailプロパティが定義されていないため、user.emailにアクセスするとTS2339エラーが発生します。

なぜこのエラーが存在するのか

TS2339エラーは、TypeScriptの型安全性を守るための重要な仕組みです。JavaScriptでは存在しないプロパティにアクセスしてもundefinedが返るだけでエラーにはなりませんが、これは以下のようなバグの温床となります。

JavaScriptでは気づけないバグ
// JavaScript: エラーにならないが、バグ!
const user = { name: '田中', age: 30 };

// タイポに気づけない
console.log(user.naem);   // undefined(バグ!)

// 存在しないプロパティへのアクセスに気づけない
console.log(user.email);  // undefined(意図した動作?)

// 実行時にようやくエラーになる
console.log(user.email.toLowerCase()); // TypeError: Cannot read properties of undefined

TypeScriptはコンパイル時にこのような問題を検出し、実行前にバグを防いでくれます。TS2339は「うるさい」と感じることもありますが、コードの品質を高めるための重要なガードレールです。

TS2339が発生する根本原因

TS2339が発生する根本的な原因は、以下の3パターンに分類できます。

パターン 原因 典型例
1. プロパティが本当にない 型定義にプロパティが含まれていない タイポ、定義忘れ
2. 型が広すぎる TypeScriptが認識している型が実際より抽象的 ユニオン型、Elementobject
3. 型が不明 TypeScriptが型を特定できない JSON.parse()、外部ライブラリ

この3パターンを意識しておくと、TS2339に遭遇したときに素早く原因を特定できるようになります。以降のセクションでは、それぞれのパターンを具体的なコード例とともに詳しく見ていきます。

基本的なケースと解決方法

まずは最も基本的なTS2339のケースと、その解決方法を見ていきましょう。これらは初心者がつまずきやすいポイントですが、原因がわかれば簡単に修正できます。

ケース1: 存在しないプロパティへのアクセス

最も単純なパターンは、オブジェクトの型に定義されていないプロパティにアクセスしようとするケースです。

NG: 存在しないプロパティへのアクセス
interface User {
  name: string;
  age: number;
}

const user: User = {
  name: '田中太郎',
  age: 30
};

// TS2339: Property 'email' does not exist on type 'User'.
console.log(user.email);

error TS2339: Property ‘email’ does not exist on type ‘User’.

原因: Userインターフェースにはnameageしか定義されていないため、emailプロパティにはアクセスできません。

解決方法: インターフェースにemailプロパティを追加します。

OK: インターフェースにプロパティを追加
interface User {
  name: string;
  age: number;
  email: string;  // プロパティを追加
}

const user: User = {
  name: '田中太郎',
  age: 30,
  email: 'tanaka@example.com'
};

console.log(user.email); // OK: "tanaka@example.com"

ポイント:プロパティが必須でない場合は、email?: stringのようにオプショナルプロパティ(?付き)にすることもできます。

ケース2: タイポ(プロパティ名のスペルミス)

実務で最も多いTS2339の原因がタイポです。プロパティ名のスペルミスは、JavaScriptではundefinedになるだけで気づけませんが、TypeScriptはコンパイル時にキャッチしてくれます。

NG: タイポによるTS2339
interface Product {
  productName: string;
  price: number;
  description: string;
}

function displayProduct(product: Product) {
  // TS2339: Property 'produtName' does not exist on type 'Product'.
  // Did you mean 'productName'?
  console.log(product.produtName);

  // TS2339: Property 'prce' does not exist on type 'Product'.
  console.log(product.prce);

  // TS2339: Property 'descrption' does not exist on type 'Product'.
  console.log(product.descrption);
}

error TS2339: Property ‘produtName’ does not exist on type ‘Product’. Did you mean ‘productName’?

解決方法: 正しいプロパティ名に修正します。TypeScriptは類似するプロパティ名を提案してくれることもあります(「Did you mean …?」)。

OK: 正しいプロパティ名を使用
function displayProduct(product: Product) {
  console.log(product.productName);  // OK
  console.log(product.price);        // OK
  console.log(product.description);  // OK
}

ポイント:VSCode等のエディタでは、Ctrl+Space(またはCmd+Space)で補完候補が表示されます。タイポを防ぐために、プロパティ名は常に補完機能を使って入力することをおすすめします。

ケース3: プロパティの追加忘れ

開発途中で新しいプロパティをオブジェクトに追加したとき、型定義(インターフェース)への追加を忘れるケースです。

NG: 型定義にプロパティを追加し忘れた
interface Config {
  apiUrl: string;
  timeout: number;
}

// 後からretryCountを追加したい
function initApp(config: Config) {
  console.log(config.apiUrl);      // OK
  console.log(config.timeout);     // OK
  // TS2339: Property 'retryCount' does not exist on type 'Config'.
  console.log(config.retryCount); // エラー!
}

error TS2339: Property ‘retryCount’ does not exist on type ‘Config’.

解決方法: インターフェースにプロパティを追加します。

OK: インターフェースを更新
interface Config {
  apiUrl: string;
  timeout: number;
  retryCount?: number; // オプショナルとして追加
}

function initApp(config: Config) {
  console.log(config.apiUrl);  // OK
  console.log(config.timeout); // OK
  console.log(config.retryCount ?? 3); // OK: undefinedの場合はデフォルト値3
}

ケース4: インターフェース定義の不足(型推論に頼りすぎ)

TypeScriptの型推論は強力ですが、オブジェクトリテラルから推論された型にはプロパティを後から追加できません。

NG: 推論された型にプロパティを追加
// 型推論: { name: string; age: number; }
const user = {
  name: '田中',
  age: 30
};

// TS2339: Property 'email' does not exist on type '{ name: string; age: number; }'.
user.email = 'tanaka@example.com';

error TS2339: Property ‘email’ does not exist on type ‘{ name: string; age: number; }’.

解決方法: 明示的にインターフェースを定義し、必要なプロパティをすべて含めます。

OK: 明示的な型定義
interface User {
  name: string;
  age: number;
  email?: string;
}

const user: User = {
  name: '田中',
  age: 30
};

user.email = 'tanaka@example.com'; // OK

ケース5: type と interface の使い分けミス

typeエイリアスで定義した型にも、同じようにTS2339が発生します。typeinterfaceはほぼ同じように使えますが、Declaration Merging(宣言のマージ)が必要な場合はinterfaceを使う必要があります。

type と interface の違い
// type では宣言のマージができない
type UserType = {
  name: string;
};
// エラー: Duplicate identifier 'UserType'.
// type UserType = { email: string; };

// interface では宣言のマージが可能
interface UserInterface {
  name: string;
}
interface UserInterface {
  email: string; // OK: マージされる
}

const user: UserInterface = {
  name: '田中',
  email: 'tanaka@example.com'
};
console.log(user.name);  // OK
console.log(user.email); // OK

ケース6: 関数の戻り値型が不足

関数の戻り値の型に必要なプロパティが含まれていない場合もTS2339が発生します。

NG: 戻り値型が不足
function getUser(): { name: string } {
  return { name: '田中' };
}

const user = getUser();
// TS2339: Property 'age' does not exist on type '{ name: string; }'.
console.log(user.age);

解決方法: 関数の戻り値型を正しく定義します。

OK: 戻り値型を正しく定義
interface User {
  name: string;
  age: number;
}

function getUser(): User {
  return { name: '田中', age: 30 };
}

const user = getUser();
console.log(user.age); // OK: 30

型の絞り込み不足によるTS2339

TS2339エラーの中で最も理解が難しく、かつ最も頻繁に遭遇するのが型の絞り込み不足によるケースです。TypeScriptは型が「広い」状態では安全でないプロパティアクセスを許可しません。Type Guard(型ガード)を使って型を絞り込む必要があります。

ユニオン型で共通でないプロパティにアクセス

ユニオン型(A | B)の変数に対しては、すべての構成型に共通するプロパティにしかアクセスできません。これはTS2339の中でも特に多いパターンです。

NG: ユニオン型の共通でないプロパティにアクセス
interface Dog {
  kind: 'dog';
  bark(): void;
}

interface Cat {
  kind: 'cat';
  meow(): void;
}

type Animal = Dog | Cat;

function makeSound(animal: Animal) {
  // OK: kind は Dog と Cat の両方に存在
  console.log(animal.kind);

  // TS2339: Property 'bark' does not exist on type 'Animal'.
  //   Property 'bark' does not exist on type 'Cat'.
  animal.bark();
}

error TS2339: Property ‘bark’ does not exist on type ‘Animal’.
Property ‘bark’ does not exist on type ‘Cat’.

原因: AnimalDog | Catなので、bark()Dogにしか存在しません。Catの場合にbark()を呼ぶと実行時エラーになるため、TypeScriptはこれをブロックします。

この問題を解決するには、型の絞り込み(Type Narrowing)を行う必要があります。以下でいくつかの方法を紹介します。

typeof による絞り込み

typeof演算子はプリミティブ型の絞り込みに使えます。string | numberのようなユニオン型でよく使います。

NG: typeof なしでプロパティにアクセス
function processValue(value: string | number) {
  // TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
  //   Property 'toUpperCase' does not exist on type 'number'.
  return value.toUpperCase();
}

error TS2339: Property ‘toUpperCase’ does not exist on type ‘string | number’.
Property ‘toUpperCase’ does not exist on type ‘number’.

OK: typeof で型を絞り込む
function processValue(value: string | number) {
  if (typeof value === 'string') {
    // この中では value は string 型
    return value.toUpperCase(); // OK
  } else {
    // この中では value は number 型
    return value.toFixed(2); // OK
  }
}

ポイント:typeofで判別できるのは 'string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'object', 'function' の8種類です。オブジェクト同士の区別には使えません。

instanceof による絞り込み

instanceof演算子はクラスのインスタンスを判別するのに使えます。DOMのElement型やErrorクラスの判別でよく使います。

NG: instanceof なしでクラス固有プロパティにアクセス
class ValidationError extends Error {
  field: string;
  constructor(message: string, field: string) {
    super(message);
    this.field = field;
  }
}

function handleError(error: Error) {
  // TS2339: Property 'field' does not exist on type 'Error'.
  console.log(error.field);
}

error TS2339: Property ‘field’ does not exist on type ‘Error’.

OK: instanceof で型を絞り込む
function handleError(error: Error) {
  if (error instanceof ValidationError) {
    // この中では error は ValidationError 型
    console.log(error.field);   // OK
    console.log(error.message); // OK
  } else {
    // この中では error は Error 型
    console.log(error.message); // OK
  }
}

in 演算子による絞り込み

in演算子は、オブジェクトにプロパティが存在するかどうかをチェックすることで型を絞り込みます。インターフェース同士の判別に便利です。

OK: in 演算子で型を絞り込む
interface Dog {
  bark(): void;
}

interface Cat {
  meow(): void;
}

function makeSound(animal: Dog | Cat) {
  if ('bark' in animal) {
    // この中では animal は Dog 型
    animal.bark(); // OK
  } else {
    // この中では animal は Cat 型
    animal.meow(); // OK
  }
}

注意:in演算子のチェック対象は文字列リテラルでなければなりません。変数 in objの形では型の絞り込みが行われません。

Discriminated Union(判別可能なユニオン)パターン

Discriminated Unionは、ユニオン型の各メンバーに共通のリテラル型プロパティ(判別子)を持たせるパターンです。TypeScriptの型システムと最も相性が良く、実務で広く使われます。

OK: Discriminated Union で安全にプロパティにアクセス
interface SuccessResponse {
  status: 'success';
  data: { id: number; name: string };
}

interface ErrorResponse {
  status: 'error';
  errorCode: number;
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  switch (response.status) {
    case 'success':
      // この中では response は SuccessResponse 型
      console.log(response.data.name); // OK
      break;
    case 'error':
      // この中では response は ErrorResponse 型
      console.log(response.errorCode); // OK
      console.log(response.message);   // OK
      break;
  }
}

Discriminated Unionのポイントは以下の3つです。

Discriminated Union の3つの条件

  • 共通のプロパティ(判別子)がすべての型に存在する
  • 判別子の値がリテラル型'success''error')である
  • switch文やif文で判別子を比較することで、型が自動的に絞り込まれる

カスタム型ガード関数

複雑な型判定ロジックがある場合は、カスタム型ガード関数を定義できます。戻り値に値 is 型という型述語を使います。

カスタム型ガード関数
interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

// カスタム型ガード関数
function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

function move(animal: Fish | Bird) {
  if (isFish(animal)) {
    // この中では animal は Fish 型
    animal.swim(); // OK
  } else {
    // この中では animal は Bird 型
    animal.fly(); // OK
  }
}

nullチェックによる絞り込み

nullundefinedとの比較もType Guardとして機能します。strictNullChecksが有効な場合(推奨設定)、nullの可能性がある型のプロパティにアクセスするとTS2339が出ることがあります。

NG: nullチェックなしでプロパティにアクセス
function getLength(value: string | null) {
  // value が null の可能性があるためエラー
  return value.length; // エラー!
}
OK: nullチェック後にアクセス
function getLength(value: string | null) {
  if (value !== null) {
    return value.length; // OK: value は string 型
  }
  return 0;
}

型の絞り込み方法の比較表

方法 用途 使える場面 構文例
typeof プリミティブ型の判別 string | number 等 typeof x === 'string'
instanceof クラスインスタンスの判別 Error, Date, DOM等 x instanceof Date
in プロパティの存在判定 インターフェース同士 'bark' in animal
Discriminated Union リテラル型判別子での判別 APIレスポンス、状態管理 switch (x.kind)
カスタム型ガード 複雑な判定ロジック 任意の型判定 function isX(v): v is X
nullチェック null/undefinedの除外 T | null, T | undefined if (x !== null)

オブジェクト操作でのTS2339

JavaScriptのオブジェクト操作メソッド(Object.keys()Object.entries()for...inループなど)は、TypeScriptの型システムとの相性が悪く、TS2339エラーが頻発するポイントです。ここでは各パターンの原因と対策を詳しく解説します。

Object.keys() の戻り値が string[]

TypeScriptのObject.keys()は、オブジェクトの型のキーではなくstring[]を返します。これは意図的な設計ですが、多くの開発者がつまずくポイントです。

NG: Object.keys() でオブジェクトにアクセス
interface User {
  name: string;
  age: number;
  email: string;
}

const user: User = { name: '田中', age: 30, email: 'tanaka@example.com' };

// Object.keys() の戻り値は string[]
const keys = Object.keys(user); // string[] であり (keyof User)[] ではない

keys.forEach(key => {
  // TS7053: Element implicitly has an 'any' type because expression
  // of type 'string' can't be used to index type 'User'.
  console.log(user[key]); // エラー!
});

error TS7053: Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘User’.

原因: Object.keys()string[]を返すのは、TypeScriptのオブジェクト型が構造的部分型であるためです。実行時には型定義にないプロパティも含む可能性があるため、keyof Tを返すのは安全ではありません。

解決方法1: keyofでキャストする。

OK: keyof でキャスト
const keys = Object.keys(user) as (keyof User)[];

keys.forEach(key => {
  console.log(user[key]); // OK: key は keyof User
});

解決方法2: 型安全なヘルパー関数を作る。

OK: 型安全なヘルパー関数
// 型安全な Object.keys
function typedKeys<T extends object>(obj: T): (keyof T)[] {
  return Object.keys(obj) as (keyof T)[];
}

typedKeys(user).forEach(key => {
  console.log(user[key]); // OK
});

注意:keyofへのキャストは、オブジェクトが型定義通りのプロパティのみを持つことが確実な場合のみ安全です。外部から渡されたオブジェクトには使わないようにしましょう。

Object.entries() の型

Object.entries()も同様に、キーの型がstringになります。

NG: Object.entries() のキーで再アクセス
interface Theme {
  primary: string;
  secondary: string;
  background: string;
}

const theme: Theme = {
  primary: '#0284c7',
  secondary: '#6d28d9',
  background: '#ffffff'
};

// [string, string][] が返る([keyof Theme, string][] ではない)
Object.entries(theme).forEach(([key, value]) => {
  // key は string 型なのでインデックスアクセスでエラー
  console.log(theme[key]); // エラー!
});

解決方法: エントリ内のvalueを直接使うか、キーをキャストします。

OK: entries の value を直接使用
// 方法1: value を直接使う(推奨)
Object.entries(theme).forEach(([key, value]) => {
  console.log(key, value); // OK: key=string, value=string
});

// 方法2: キーをキャスト
(Object.entries(theme) as [keyof Theme, string][]).forEach(([key, value]) => {
  console.log(theme[key]); // OK
});

for…in ループでのプロパティアクセス

for...inループでも同じ問題が発生します。ループ変数の型はstringになります。

NG: for…in のキーでアクセス
interface Config {
  host: string;
  port: number;
}

const config: Config = { host: 'localhost', port: 3000 };

for (const key in config) {
  // key は string 型
  console.log(config[key]); // エラー!
}
OK: for…in のキーをキャスト
for (const key in config) {
  const typedKey = key as keyof Config;
  console.log(config[typedKey]); // OK
}

ポイント:オブジェクトのイテレーションでは、for...inよりもObject.entries()を使い、分割代入で直接値を取得する方がスッキリ書けます。

スプレッド演算子での問題

スプレッド演算子でオブジェクトを結合した場合、結合後の型から元の型固有のプロパティにアクセスできないことがあります。

スプレッド演算子の型推論
interface BaseUser {
  name: string;
}

interface UserDetails {
  age: number;
  email: string;
}

function mergeUser(base: BaseUser, details: UserDetails) {
  const merged = { ...base, ...details };
  // OK: TypeScriptは正しく推論する
  console.log(merged.name);  // OK
  console.log(merged.age);   // OK
  console.log(merged.email); // OK
  return merged;
}

// ただし、戻り値の型を明示すると問題になることがある
function mergeUserTyped(base: BaseUser, details: UserDetails): BaseUser {
  const merged = { ...base, ...details };
  return merged; // OK(余剰プロパティは許可される)
}

const result = mergeUserTyped({ name: '田中' }, { age: 30, email: 'a@b.com' });
// TS2339: Property 'age' does not exist on type 'BaseUser'.
console.log(result.age); // エラー! 戻り値型が BaseUser なので

解決方法: 結合後の型を正しく定義します。

OK: 正しい戻り値型を定義
// 方法1: 交差型を使う
function mergeUser(base: BaseUser, details: UserDetails): BaseUser & UserDetails {
  return { ...base, ...details };
}

const result = mergeUser({ name: '田中' }, { age: 30, email: 'a@b.com' });
console.log(result.name);  // OK
console.log(result.age);   // OK
console.log(result.email); // OK

動的プロパティアクセス(ブラケット記法)

変数を使ったブラケット記法によるプロパティアクセスでも、キーの型が適切でないとTS2339(またはTS7053)が発生します。

NG: 動的プロパティアクセスの型不整合
interface Translations {
  hello: string;
  goodbye: string;
  thanks: string;
}

const t: Translations = {
  hello: 'こんにちは',
  goodbye: 'さようなら',
  thanks: 'ありがとう'
};

function translate(key: string) {
  return t[key]; // エラー: string は Translations のインデックスに使えない
}
OK: keyof で引数の型を制限
function translate(key: keyof Translations) {
  return t[key]; // OK: key は 'hello' | 'goodbye' | 'thanks'
}

translate('hello');   // OK: "こんにちは"
translate('goodbye'); // OK: "さようなら"
// translate('unknown'); // エラー: 'unknown' は割り当て不可

Record型を使ったインデックスアクセス

プロパティ名が動的に決まる場合は、Record型やインデックスシグネチャを使うのが適切です。

Record型とインデックスシグネチャ
// Record型: 任意のキーに対応するオブジェクト
const cache: Record<string, unknown> = {};
cache['any-key'] = 'any-value'; // OK
cache.anotherKey = 42;             // OK

// インデックスシグネチャ: 同様の効果
interface FlexibleObject {
  id: number;            // 固定プロパティ
  [key: string]: unknown; // 任意のプロパティも許可
}

const obj: FlexibleObject = { id: 1 };
obj.customProp = 'value'; // OK
obj.anything = 42;        // OK

配列・DOM操作でのTS2339

フロントエンド開発では、DOM要素の操作や配列メソッドの使用時にTS2339が頻繁に発生します。これはDOMのAPIが返す型が抽象的であることが主な原因です。

querySelector の戻り値が Element | null

document.querySelector()の戻り値はElement | nullです。Element型にはvaluestyleなどのHTML固有のプロパティがなく、TS2339が発生します。

NG: querySelector の戻り値で直接プロパティにアクセス
// querySelector の戻り値は Element | null
const input = document.querySelector('#email');

// TS2339: Property 'value' does not exist on type 'Element'.
console.log(input.value);

// TS2339: Property 'style' does not exist on type 'Element'.
input.style.color = 'red';

// TS2339: Property 'href' does not exist on type 'Element'.
const link = document.querySelector('a');
console.log(link.href);

error TS2339: Property ‘value’ does not exist on type ‘Element’.

解決方法1: ジェネリクスで型を指定する(推奨)。

OK: ジェネリクスで型を指定
// HTMLInputElement として取得
const input = document.querySelector<HTMLInputElement>('#email');
if (input) {
  console.log(input.value); // OK
}

// HTMLAnchorElement として取得
const link = document.querySelector<HTMLAnchorElement>('a.nav-link');
if (link) {
  console.log(link.href); // OK
}

解決方法2: instanceofで型を絞り込む。

OK: instanceof で絞り込み
const el = document.querySelector('#email');

if (el instanceof HTMLInputElement) {
  console.log(el.value); // OK
  el.disabled = true;  // OK
}

if (el instanceof HTMLSelectElement) {
  console.log(el.selectedIndex); // OK
}

よく使うHTMLElement型の対応表

HTML要素 TypeScript型 固有プロパティ例
<input> HTMLInputElement value, checked, disabled
<select> HTMLSelectElement value, selectedIndex, options
<textarea> HTMLTextAreaElement value, rows, cols
<a> HTMLAnchorElement href, target, download
<img> HTMLImageElement src, alt, naturalWidth
<form> HTMLFormElement action, method, submit()
<canvas> HTMLCanvasElement getContext(), width, height
<video> HTMLVideoElement play(), pause(), duration
<button> HTMLButtonElement disabled, type, form
任意の要素 HTMLElement style, classList, dataset

HTMLElement 固有のプロパティ

Element型とHTMLElement型の違いも重要です。styleプロパティはHTMLElementにしか存在しないため、Element型のままだとTS2339が出ます。

Element vs HTMLElement
// querySelectorAll は NodeListOf<Element> を返す
const elements = document.querySelectorAll('.card');

elements.forEach(el => {
  // TS2339: Property 'style' does not exist on type 'Element'.
  el.style.display = 'none'; // エラー!
});

// 解決: ジェネリクスで HTMLElement を指定
const cards = document.querySelectorAll<HTMLDivElement>('.card');

cards.forEach(el => {
  el.style.display = 'none'; // OK
});

NodeList vs HTMLCollection

DOM APIにはNodeListHTMLCollectionの2種類のコレクションがあり、使えるメソッドが異なります。

NodeList と HTMLCollection の違い
// querySelectorAll → NodeList(forEach あり)
const nodeList = document.querySelectorAll('.item');
nodeList.forEach(el => { /* OK */ });

// getElementsByClassName → HTMLCollection(forEach なし!)
const collection = document.getElementsByClassName('item');
// TS2339: Property 'forEach' does not exist on type 'HTMLCollectionOf<Element>'.
collection.forEach(el => { }); // エラー!

// 解決: Array.from で配列に変換
Array.from(collection).forEach(el => {
  console.log(el.className); // OK
});

error TS2339: Property ‘forEach’ does not exist on type ‘HTMLCollectionOf<Element>’.

イベントオブジェクトの型

イベントハンドラのeventオブジェクトの型も、TS2339の温床です。特にevent.targetEventTarget | null型であり、HTML要素固有のプロパティにアクセスできません。

NG: event.target のプロパティにアクセス
document.addEventListener('click', (event) => {
  // TS2339: Property 'value' does not exist on type 'EventTarget'.
  console.log(event.target.value);

  // TS2339: Property 'classList' does not exist on type 'EventTarget'.
  event.target.classList.add('active');
});
OK: event.target を適切な型に絞り込む
// 方法1: instanceof で絞り込み
document.addEventListener('click', (event) => {
  if (event.target instanceof HTMLInputElement) {
    console.log(event.target.value); // OK
  }
  if (event.target instanceof HTMLElement) {
    event.target.classList.add('active'); // OK
  }
});

// 方法2: event.currentTarget を使う(リスナーが設定された要素)
const btn = document.querySelector<HTMLButtonElement>('#submit');
btn?.addEventListener('click', (event) => {
  const button = event.currentTarget as HTMLButtonElement;
  button.disabled = true; // OK
});

Array メソッドの戻り値型

配列メソッドの戻り値型にも注意が必要です。find()undefinedを含む型を返すため、プロパティアクセスでエラーになることがあります。

Array メソッドの戻り値に注意
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: '田中' },
  { id: 2, name: '佐藤' }
];

// find() の戻り値は User | undefined
const user = users.find(u => u.id === 1);

// エラー: user が undefined の可能性
console.log(user.name); // エラー!

// 解決方法
if (user) {
  console.log(user.name); // OK
}

// またはOptional chaining
console.log(user?.name); // OK: User が見つからなければ undefined

// Non-null assertion(確実に存在する場合のみ)
console.log(user!.name); // OK(ただし危険)

注意:Non-null assertion演算子(!)は「この値はnullでもundefinedでもない」とTypeScriptに伝えるものですが、実行時のチェックは行いません。使用は最小限にとどめ、if文やOptional chainingを優先しましょう。

クラスでのTS2339

TypeScriptのクラスでは、アクセス修飾子(private/protected)や型宣言の不足が原因でTS2339が発生することがあります。

private/protected プロパティへの外部アクセス

privateprotectedで宣言されたプロパティに外部からアクセスしようとすると、TS2339ではなくTS2341(private)やTS2445(protected)が発生しますが、状況によってはTS2339として表示されることもあります。

NG: private プロパティへの外部アクセス
class BankAccount {
  private balance: number;
  protected accountNumber: string;

  constructor(balance: number, accountNumber: string) {
    this.balance = balance;
    this.accountNumber = accountNumber;
  }
}

const account = new BankAccount(10000, '1234-5678');

// エラー: private プロパティにはアクセスできない
console.log(account.balance);       // エラー!
console.log(account.accountNumber); // エラー!

解決方法: ゲッターメソッド(公開API)を用意する。

OK: ゲッターを使って公開
class BankAccount {
  private _balance: number;

  constructor(balance: number) {
    this._balance = balance;
  }

  // ゲッターで読み取り専用として公開
  get balance(): number {
    return this._balance;
  }
}

const account = new BankAccount(10000);
console.log(account.balance); // OK: 10000

初期化されていないプロパティ

strictPropertyInitialization(推奨設定)が有効な場合、コンストラクタで初期化されていないプロパティはTS2564エラーになりますが、それとは別に、型宣言を忘れたプロパティにアクセスするとTS2339が発生します。

NG: 型宣言なしでプロパティを使用
class Timer {
  constructor() {
    // TS2339: Property 'intervalId' does not exist on type 'Timer'.
    this.intervalId = null; // エラー!
  }

  start() {
    // TS2339: Property 'intervalId' does not exist on type 'Timer'.
    this.intervalId = setInterval(() => {}, 1000);
  }
}

error TS2339: Property ‘intervalId’ does not exist on type ‘Timer’.

OK: プロパティを明示的に宣言
class Timer {
  private intervalId: ReturnType<typeof setInterval> | null = null;

  start() {
    this.intervalId = setInterval(() => {
      console.log('tick');
    }, 1000);
  }

  stop() {
    if (this.intervalId !== null) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
}

動的プロパティの追加

JavaScriptではオブジェクトに自由にプロパティを追加できますが、TypeScriptのクラスでは事前に宣言していないプロパティは追加できません。

NG: クラスに動的プロパティを追加
class DynamicConfig {
  setOption(key: string, value: unknown) {
    // TS2339 or TS7053: 動的プロパティ追加はできない
    this[key] = value; // エラー!
  }
}

解決方法: インデックスシグネチャを追加するか、Mapを使います。

OK: Map を使って動的プロパティを管理
// 方法1: Map を使う(推奨)
class DynamicConfig {
  private options = new Map<string, unknown>();

  setOption(key: string, value: unknown) {
    this.options.set(key, value); // OK
  }

  getOption(key: string): unknown {
    return this.options.get(key); // OK
  }
}

// 方法2: インデックスシグネチャを使う
class FlexibleConfig {
  [key: string]: unknown;

  setOption(key: string, value: unknown) {
    this[key] = value; // OK
  }
}

継承元にないプロパティ

親クラスの型を通じてサブクラスのインスタンスにアクセスすると、サブクラス固有のプロパティでTS2339が発生します。

NG: 親クラス型でサブクラスのプロパティにアクセス
class Shape {
  color: string;
  constructor(color: string) {
    this.color = color;
  }
}

class Circle extends Shape {
  radius: number;
  constructor(color: string, radius: number) {
    super(color);
    this.radius = radius;
  }
}

function getArea(shape: Shape) {
  // TS2339: Property 'radius' does not exist on type 'Shape'.
  return Math.PI * shape.radius ** 2; // エラー!
}

error TS2339: Property ‘radius’ does not exist on type ‘Shape’.

OK: instanceof で型を絞り込む / 引数の型を具体的にする
// 方法1: instanceof で絞り込み
function getArea(shape: Shape): number {
  if (shape instanceof Circle) {
    return Math.PI * shape.radius ** 2; // OK
  }
  return 0;
}

// 方法2: 引数の型を具体的にする
function getCircleArea(circle: Circle): number {
  return Math.PI * circle.radius ** 2; // OK
}

外部ライブラリ・API連携でのTS2339

外部のAPIやライブラリとの連携では、TypeScriptが型情報を把握できないためにTS2339が頻繁に発生します。このセクションでは、実務で遭遇する代表的なパターンと対策を解説します。

JSON.parse() の戻り値が any

JSON.parse()の戻り値型はanyです。any型は型チェックをバイパスするため、TS2339は出ませんが、unknownとして扱った場合やnoImplicitAnyの設定によっては問題になります。

JSON.parse() の型安全な使い方
interface ApiData {
  userId: number;
  title: string;
  completed: boolean;
}

// 方法1: 型アサーション(簡易的)
const data = JSON.parse(jsonString) as ApiData;
console.log(data.title); // OK

// 方法2: バリデーション関数(推奨)
function isApiData(data: unknown): data is ApiData {
  return (
    typeof data === 'object' &&
    data !== null &&
    'userId' in data &&
    'title' in data &&
    'completed' in data
  );
}

const parsed: unknown = JSON.parse(jsonString);
if (isApiData(parsed)) {
  console.log(parsed.title); // OK: 型安全
}

fetch/axios のレスポンス型

fetchのレスポンスを.json()で取得すると、戻り値はany(またはunknown)になります。そのままプロパティにアクセスするとTS2339が発生します。

NG: fetch のレスポンスを型なしで使用
// strictモードでは response.json() が unknown を返す場合がある
async function fetchUser() {
  const response = await fetch('/api/user');
  const data = await response.json();

  // any型なら通るが、型安全ではない
  // unknown型なら TS2339 が発生
  console.log(data.name); // 型安全ではない
}
OK: fetch のレスポンスに型を付ける
interface User {
  id: number;
  name: string;
  email: string;
}

// 方法1: 型アサーション
async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  const data: User = await response.json();
  return data;
}

// 方法2: ジェネリックなfetchラッパー(推奨)
async function typedFetch<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json() as Promise<T>;
}

// 使い方
const user = await typedFetch<User>('/api/user');
console.log(user.name);  // OK
console.log(user.email); // OK

window オブジェクトのカスタムプロパティ

サードパーティのスクリプト(Google Analytics、チャットウィジェットなど)がwindowオブジェクトにグローバル変数を追加するケースは実務で非常に多く、TS2339の頻出パターンです。

NG: window のカスタムプロパティにアクセス
// TS2339: Property 'gtag' does not exist on type 'Window & typeof globalThis'.
window.gtag('event', 'page_view');

// TS2339: Property 'myApp' does not exist on type 'Window & typeof globalThis'.
window.myApp = { version: '1.0.0' };

error TS2339: Property ‘gtag’ does not exist on type ‘Window & typeof globalThis’.

解決方法: Windowインターフェースを拡張する型宣言ファイルを作成します。

OK: global.d.ts で Window インターフェースを拡張
// global.d.ts(プロジェクトルートに配置)
declare global {
  interface Window {
    gtag: (...args: unknown[]) => void;
    myApp: {
      version: string;
    };
  }
}

export {}; // モジュールとして扱うために必要
これで window のカスタムプロパティにアクセス可能
window.gtag('event', 'page_view'); // OK
window.myApp = { version: '1.0.0' }; // OK

グローバル変数の型定義

CDNから読み込んだライブラリのグローバル変数にも同様の問題が発生します。

グローバル変数の型宣言
// global.d.ts

// CDNから読み込んだライブラリのグローバル変数
declare const _: typeof import('lodash');
declare const $: typeof import('jquery');

// 独自グローバル変数
declare const API_BASE_URL: string;
declare const IS_PRODUCTION: boolean;

@types パッケージの不足

JavaScriptで書かれたnpmパッケージを使う場合、型定義がないとモジュール全体がany型になったり、TS2339が発生したりします。

@types パッケージのインストール
# 型定義がないライブラリでエラーが出る場合
# まず @types パッケージがあるか確認
npm install --save-dev @types/lodash
npm install --save-dev @types/express
npm install --save-dev @types/node

# @types がない場合は、独自の型宣言ファイルを作成
# src/types/some-library.d.ts
declare module 'some-untyped-library' {
  export function doSomething(input: string): string;
  export default class SomeClass {
    constructor(options: Record<string, unknown>);
    run(): void;
  }
}

ポイント:TypeScriptの@typesパッケージはDefinitelyTypedリポジトリで管理されています。npm search @types/ライブラリ名で対応する型定義を検索できます。

process.env の型

Node.jsのprocess.envのプロパティはstring | undefined型です。環境変数名のタイポでもTS2339は出ませんが(インデックスシグネチャがあるため)、型を厳密にしたい場合は環境変数の型定義を作成します。

process.env の型を厳密にする
// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    DATABASE_URL: string;
    API_KEY: string;
    PORT?: string;
  }
}

// 使用例
const dbUrl = process.env.DATABASE_URL; // string 型(undefinedなし)
const port = process.env.PORT;         // string | undefined

React/Vue でのTS2339

ReactやVueなどのフロントエンドフレームワークでは、コンポーネントのPropsStateイベントRefまわりでTS2339が頻繁に発生します。ここではReactを中心に、代表的なパターンと解決方法を紹介します。

React: Props の型不足

Reactコンポーネントで最も多いTS2339は、Propsの型定義が不足しているケースです。

NG: Props の型にプロパティが不足
interface ButtonProps {
  label: string;
}

const Button = ({ label, onClick }: ButtonProps) => {
  // TS2339: Property 'onClick' does not exist on type 'ButtonProps'.
  return <button onClick={onClick}>{label}</button>;
};

error TS2339: Property ‘onClick’ does not exist on type ‘ButtonProps’.

OK: Props に必要なプロパティを追加
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

const Button = ({ label, onClick, disabled, variant = 'primary' }: ButtonProps) => {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
};

React: event.target のプロパティ

Reactのイベントハンドラでも、event.targetのプロパティアクセスでTS2339が発生します。Reactの合成イベントには専用の型が用意されています。

NG: event.target のプロパティに直接アクセス
const Form = () => {
  const handleChange = (e: React.ChangeEvent) => {
    // TS2339: Property 'value' does not exist on type 'EventTarget'.
    console.log(e.target.value);
  };

  return <input onChange={handleChange} />;
};
OK: ジェネリクスで要素の型を指定
const Form = () => {
  // ChangeEvent にジェネリクスで要素の型を指定
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value); // OK
  };

  const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    console.log(e.target.value);         // OK
    console.log(e.target.selectedIndex); // OK
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // currentTarget は常に HTMLFormElement 型
    console.log(e.currentTarget.action); // OK
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <select onChange={handleSelectChange}>
        <option>A</option>
      </select>
    </form>
  );
};

React: Reactのイベント型一覧

イベント React型 よく使うジェネリクス
onChange ChangeEvent<T> HTMLInputElement, HTMLSelectElement
onClick MouseEvent<T> HTMLButtonElement, HTMLDivElement
onSubmit FormEvent<T> HTMLFormElement
onKeyDown KeyboardEvent<T> HTMLInputElement
onFocus/onBlur FocusEvent<T> HTMLInputElement
onDrag DragEvent<T> HTMLDivElement

React: ref の型

useRefのジェネリクスに正しい型を指定しないと、ref.currentのプロパティアクセスでTS2339が発生します。

NG: useRef の型指定なし
const inputRef = useRef(null);

// TS2339: Property 'value' does not exist on type 'never'.
console.log(inputRef.current.value);
OK: useRef にジェネリクスで型を指定
import { useRef } from 'react';

// DOM要素の ref
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

// null チェックが必要
if (inputRef.current) {
  console.log(inputRef.current.value); // OK
  inputRef.current.focus();            // OK
}

// 値の保持用 ref(DOM以外)
const countRef = useRef<number>(0);
countRef.current += 1; // OK: number型

React: カスタムフックの戻り値

カスタムフックの戻り値の型定義が不足している場合もTS2339が発生します。

カスタムフックの戻り値に正しい型を付ける
// 戻り値の型を明示的に定義
interface UseFormReturn<T> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  handleSubmit: (e: React.FormEvent) => void;
  isValid: boolean;
}

function useForm<T>(initialValues: T): UseFormReturn<T> {
  // 実装...
}

// 使用側: すべてのプロパティに安全にアクセスできる
const { values, errors, handleChange, handleSubmit, isValid } = useForm({
  email: '',
  password: ''
});

console.log(values.email);    // OK
console.log(values.password); // OK
console.log(isValid);          // OK

Vue 3: Composition API でのTS2339

Vue 3のComposition APIでも同様の問題が発生します。

Vue 3: ref と reactive の型
import { ref, reactive } from 'vue';

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

// ref: ジェネリクスで型を指定
const user = ref<User | null>(null);

// null チェックが必要
if (user.value) {
  console.log(user.value.name); // OK
}

// reactive: 型推論が効く
const state = reactive<{ users: User[]; loading: boolean }>({
  users: [],
  loading: false
});

state.users.push({ name: '田中', age: 30 }); // OK
state.loading = true; // OK

Vue 3: テンプレートref の型

Vue 3: テンプレートref の型付け
import { ref, onMounted } from 'vue';

// テンプレートref は InstanceType または HTMLElement で型付け
const inputEl = ref<HTMLInputElement | null>(null);

onMounted(() => {
  if (inputEl.value) {
    inputEl.value.focus();              // OK
    console.log(inputEl.value.value); // OK
  }
});

正しい解決アプローチ

TS2339エラーの解決方法は複数ありますが、すべてが等しく良い方法というわけではありません。ここでは、安全性と実用性のバランスが取れた推奨アプローチを紹介します。

インターフェースの拡張(extends)

既存の型に新しいプロパティを追加したい場合は、extendsでインターフェースを拡張するのが最もクリーンな方法です。

インターフェースの拡張
// ベースの型
interface BaseUser {
  id: number;
  name: string;
}

// 拡張(新しいプロパティを追加)
interface DetailedUser extends BaseUser {
  email: string;
  role: 'admin' | 'user';
}

// 複数のインターフェースを同時に拡張
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface FullUser extends DetailedUser, Timestamped {
  lastLogin: Date;
}

// FullUser は id, name, email, role, createdAt, updatedAt, lastLogin を持つ
const user: FullUser = {
  id: 1,
  name: '田中',
  email: 'tanaka@example.com',
  role: 'admin',
  createdAt: new Date(),
  updatedAt: new Date(),
  lastLogin: new Date()
};

型ガードの活用

実行時に値をチェックして型を絞り込む型ガードは、TS2339の最も安全な解決方法です。再利用可能な型ガード関数を作っておくと、プロジェクト全体で一貫した型チェックが行えます。

再利用可能な型ガード関数
// nullやundefinedを除外する型ガード
function isDefined<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// オブジェクトにプロパティが存在するかチェックする型ガード
function hasProperty<T extends object, K extends string>(
  obj: T,
  key: K
): obj is T & Record<K, unknown> {
  return key in obj;
}

// 使用例
const data: unknown = JSON.parse('{"name": "田中"}');

if (
  typeof data === 'object' &&
  data !== null &&
  hasProperty(data, 'name')
) {
  console.log(data.name); // OK: unknown 型
}

インデックスシグネチャと Record 型

動的なキーを持つオブジェクトには、インデックスシグネチャまたはRecordを使います。

インデックスシグネチャと Record 型の使い分け
// Record型: すべてのキーが同じ値型
const scores: Record<string, number> = {};
scores['math'] = 90;    // OK
scores['english'] = 85; // OK
scores.science = 95;     // OK

// Record型: キーを制限する
type Subject = 'math' | 'english' | 'science';
const limitedScores: Record<Subject, number> = {
  math: 90,
  english: 85,
  science: 95
};

// インデックスシグネチャ: 固定プロパティ + 動的プロパティ
interface CacheStore {
  version: number;           // 固定(常に存在)
  [key: string]: unknown;    // 動的(任意のキー)
}

Optional chaining(?.)との関係

Optional chaining(?.)はnull/undefinedの安全なアクセスには使えますが、TS2339は解決しません。これは重要な違いです。

Optional chaining は TS2339 を解決しない
interface User {
  name: string;
}

const user: User = { name: '田中' };

// TS2339: Property 'email' does not exist on type 'User'.
console.log(user?.email); // ?. を使ってもエラーは出る

// Optional chaining が有効なのは null/undefined の可能性がある場合
const maybeUser: User | null = null;
console.log(maybeUser?.name); // OK: null の場合は undefined を返す

注意:?.(Optional chaining)は「値がnullかundefinedならundefinedを返す」演算子です。「型にないプロパティへのアクセスを許可する」ものではありません。TS2339を解決するには、型定義自体を修正する必要があります。

型アサーション(as)はいつ使うべきか

型アサーション(as)はTypeScriptに「この値はこの型だ」と伝える機能ですが、実行時のチェックは行いません。使い方を誤るとバグの原因になります。

型アサーション(as)の正しい使い方と危険な使い方
// NG: 根拠のないアサーション(危険)
const data = {} as { name: string; age: number };
console.log(data.name.toUpperCase()); // コンパイルは通るが実行時エラー!

// OK: DOMのジェネリクスの代わりに使う
const input = document.getElementById('email') as HTMLInputElement;

// OK: APIレスポンスの型を指定(バリデーション済みの場合)
const response = await fetch('/api/user');
const user = (await response.json()) as User;

// OK: Object.keys のキャスト
const keys = Object.keys(obj) as (keyof typeof obj)[];

TS2339 解決方法の優先順位

TS2339を解決する方法には、安全性の高いものから低いものまで段階があります。以下の優先順位で対処することをおすすめします。

優先順位 方法 安全性 説明
1 型定義の修正 最高 インターフェースにプロパティを追加する(正しいプロパティなら)
2 型ガード / 絞り込み 高い typeof, instanceof, in演算子, カスタム型ガード
3 ジェネリクス 高い querySelector<T>, useRef<T> など
4 型アサーション (as) 中程度 開発者が型を保証する(バリデーション併用推奨)
5 any へのキャスト 低い 型チェックを完全に無効化(最後の手段)
6 @ts-ignore 最低 エラーを無視する(絶対に避けるべき)

注意:// @ts-ignoreas anyはTS2339を「黙らせる」ことはできますが、型安全性を失います。これらはあくまで一時的な回避策であり、本番コードには残さないようにしましょう。

実務でよくあるTS2339パターン10選

ここでは、実際のプロジェクトで最も頻繁に遭遇するTS2339パターンを10個厳選し、エラーメッセージ・原因・解決方法をまとめます。

パターン1: APIレスポンスの型定義不足

error TS2339: Property ‘data’ does not exist on type ‘{}’.

APIレスポンスの型定義
// NG: 型なし
const response = await fetch('/api/users');
const result = await response.json();
console.log(result.data); // any型 or エラー

// OK: レスポンス型を定義
interface ApiResponse<T> {
  data: T;
  message: string;
  status: number;
}

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

const result = await typedFetch<ApiResponse<User[]>>('/api/users');
console.log(result.data);     // OK: User[]
console.log(result.message);  // OK: string

パターン2: window.XXX(カスタムグローバル変数)

error TS2339: Property ‘dataLayer’ does not exist on type ‘Window & typeof globalThis’.

Google Tag Manager の dataLayer
// global.d.ts に以下を追加
declare global {
  interface Window {
    dataLayer: Record<string, unknown>[];
    gtag: (...args: unknown[]) => void;
  }
}
export {};

// これで window.dataLayer にアクセス可能
window.dataLayer.push({ event: 'pageview' }); // OK

パターン3: document.querySelector の型

error TS2339: Property ‘value’ does not exist on type ‘Element’.

querySelector のベストプラクティス
// パターンA: ジェネリクスで型指定(最もシンプル)
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
if (input) input.value = 'test@example.com';

// パターンB: getElementById + as(IDセレクタの場合)
const form = document.getElementById('myForm') as HTMLFormElement | null;
if (form) form.submit();

パターン4: Object.keys のイテレーション

error TS7053: Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘Config’.

Object.keys イテレーションのベストプラクティス
interface Config {
  host: string;
  port: number;
  debug: boolean;
}

const config: Config = { host: 'localhost', port: 3000, debug: true };

// 推奨: Object.entries で値を直接取得
Object.entries(config).forEach(([key, value]) => {
  console.log(`${key}: ${value}`); // OK
});

パターン5: JSON.parse の戻り値

JSON.parse を安全に使う
// 方法1: 型アサーション + 戻り値型の明示
interface Settings {
  theme: 'light' | 'dark';
  fontSize: number;
}

function loadSettings(): Settings {
  const raw = localStorage.getItem('settings');
  if (!raw) return { theme: 'light', fontSize: 14 };
  return JSON.parse(raw) as Settings;
}

const settings = loadSettings();
console.log(settings.theme);    // OK
console.log(settings.fontSize); // OK

パターン6: イベントオブジェクトのプロパティ

error TS2339: Property ‘value’ does not exist on type ‘EventTarget’.

イベントターゲットの型付け
// Vanilla JS
document.querySelector('input')?.addEventListener('input', (e) => {
  const target = e.target as HTMLInputElement;
  console.log(target.value); // OK
});

// React
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value); // OK
};

パターン7: ライブラリの型更新後

ライブラリをアップデートした際に、型定義が変更されてTS2339が発生することがあります。

ライブラリ更新後の対処
# 型定義だけ更新されている可能性をチェック
npm ls @types/react

# ライブラリのCHANGELOGやMigration Guideを確認
# プロパティ名の変更やAPIの廃止が原因のことが多い

# 型定義を最新に更新
npm update @types/react @types/react-dom

パターン8: 環境変数 process.env

環境変数の安全なアクセス
// process.env.XXX は string | undefined
const port = process.env.PORT;

// undefined の可能性があるので、デフォルト値を設定
const port = process.env.PORT ?? '3000';
const portNum = parseInt(port, 10); // OK

// 必須の環境変数をチェックする関数
function getRequiredEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`環境変数 ${key} が設定されていません`);
  }
  return value;
}

const dbUrl = getRequiredEnv('DATABASE_URL'); // string型が保証される

パターン9: Map/Set の型

Map と Set の型付け
// Map: get() は V | undefined を返す
const userMap = new Map<number, User>();
userMap.set(1, { id: 1, name: '田中' });

const user = userMap.get(1); // User | undefined

// undefined チェックが必要
if (user) {
  console.log(user.name); // OK
}

// Set: has() で存在チェック、forEach で反復
const idSet = new Set<number>([1, 2, 3]);
idSet.has(1); // true
idSet.forEach(id => console.log(id)); // OK: number型

パターン10: 条件分岐後の型絞り込みが効かない

TypeScriptの制御フロー解析は、一部のケースで型の絞り込みが期待通りに動作しないことがあります。

型の絞り込みが効かないケースと対処法
interface User {
  name: string;
  address?: { city: string };
}

const user: User = { name: '田中' };

// NG: コールバック内では型の絞り込みが効かない
if (user.address) {
  setTimeout(() => {
    // user.address が undefined に変更される可能性があるため
    // コールバック内では絞り込みが効かないことがある
    console.log(user.address.city); // エラーの場合あり
  }, 1000);
}

// OK: ローカル変数に代入して使う
const address = user.address;
if (address) {
  setTimeout(() => {
    console.log(address.city); // OK: ローカル変数は変更されない
  }, 1000);
}

まとめ

この記事では、TypeScriptのTS2339エラー(Property 'X' does not exist on type 'Y')について、発生する全パターンと解決方法を解説しました。最後に、重要なポイントをまとめます。

TS2339 解決のポイントまとめ

  • 根本原因は3パターン: プロパティが本当にない、型が広すぎる、型が不明
  • 最も多い原因はタイポ: エディタの補完機能を活用して防ぐ
  • ユニオン型は絞り込みが必須: typeof / instanceof / in / Discriminated Union を使う
  • Object.keys() は string[] を返す: keyof キャストまたは Object.entries() を使う
  • DOM操作はジェネリクスで型指定: querySelector<HTMLInputElement>
  • window のカスタムプロパティは global.d.ts で Window インターフェースを拡張
  • 型アサーション(as)は最後の手段: まず型定義の修正や型ガードを検討する
  • @ts-ignore は使わない: 根本的な解決にならず、バグの温床になる

TS2339 vs TS2322 vs TS2345 の違い

TypeScriptのエラーコードは似たものがいくつかあります。それぞれの違いを理解しておくと、エラーに遭遇したときに素早く対処できます。

エラーコード エラーメッセージ 意味 典型例
TS2339 Property ‘X’ does not exist on type ‘Y’ 型にないプロパティにアクセス user.email(emailが型にない)
TS2322 Type ‘X’ is not assignable to type ‘Y’ 型Xを型Yに代入できない let x: string = 42
TS2345 Argument of type ‘X’ is not assignable to parameter of type ‘Y’ 関数の引数の型が合わない fn(42)(stringを期待)
TS7053 Element implicitly has an ‘any’ type インデックスアクセスの型が不明 obj[key](keyがstring型)

関連記事

TypeScriptの型システムについてさらに深く学びたい方は、以下の記事も参考にしてください。

関連記事