エラーの内容
イベントリスナーを設定して event.target.value を読み取ろうとすると、TypeScript が即座にこんなエラーを出してきます:
input.addEventListener('change', (event) => {
console.log(event.target.value); // ❌ Property 'value' does not exist on type 'EventTarget'.
});
React でも同じ壁にぶつかります:
const handleChange = (event: React.ChangeEvent) => {
console.log(event.target.value); // ❌ 同じエラー
};
なぜこのエラーが起きるのか
event.target の型は EventTarget — 最も汎用的な DOM インターフェースです。保証されているメソッドは addEventListener、dispatchEvent、removeEventListener のわずか数個だけです。value、checked、files、その他の要素固有のプロパティは含まれていません。
TypeScript は EventTarget を自動的に HTMLInputElement に絞り込んでくれません。型レベルでは、イベントは あらゆる 要素(<div>、<span>、シャドウ DOM ノードなど)で発生し得るからです。コンパイラに「実際に何を扱っているか」を明示する必要があります。
修正方法 1 — 型アサーション(最速の修正)
event.target を想定している要素の型にキャストします:
input.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
console.log(target.value); // ✅
});
要素に合わせてインターフェースを選びましょう:
HTMLInputElement—<input>HTMLTextAreaElement—<textarea>HTMLSelectElement—<select>HTMLButtonElement—<button>
React ではさらにすっきり書けます。イベント型のジェネリックパラメータに要素の型を渡せば、キャスト自体が不要になります:
// React — ジェネリックパラメータで型付けを解決
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value); // ✅ キャスト不要
};
<input type="text" onChange={handleChange} />
修正方法 2 — target の代わりに currentTarget を使う
キャストを完全に避けられる、より細かい修正方法もあります。currentTarget はリスナーがアタッチされている要素を指します。TypeScript は addEventListener を呼び出した要素からその型を推論するため、正確な型付けが自動で得られます。
const input = document.querySelector('input'); // HTMLInputElement | null
input?.addEventListener('change', (event) => {
// event.currentTarget は HTMLInputElement — キャスト不要
console.log(event.currentTarget?.value); // ✅
});
この違いが重要な理由を説明します。target はユーザーが実際にクリックまたは入力した要素であり、コンテナの内側にネストされた子要素である可能性があります。一方 currentTarget は常にリスナーを保持している要素です。フォームフィールドでは、ほぼ常に currentTarget を選ぶのが正解です。
修正方法 3 — 型ガード(共有ハンドラーに最も安全)
複数の要素型をまとめて扱うハンドラーには instanceof ガードを使いましょう。型を絞り込みつつ、実行時の安全性も確保できます:
function handleEvent(event: Event) {
if (event.target instanceof HTMLInputElement) {
console.log(event.target.value); // ✅ ここでは TypeScript が型を認識している
} else if (event.target instanceof HTMLSelectElement) {
console.log(event.target.value); // ✅
}
}
document.querySelector('form')?.addEventListener('change', handleEvent);
フォーム内のターゲットがボタンや他の想定外の要素だった場合も、ガードがきれいに処理してくれるので、実行時にクラッシュしません。
修正方法 4 — 型付きイベントハンドラーヘルパー(再利用可能なパターン)
同じキャストを何十ものファイルに書くのはすぐに面倒になります。一度だけ抽出して、どこでも再利用しましょう:
function inputHandler(fn: (value: string, event: Event) => void) {
return (event: Event) => {
if (event.target instanceof HTMLInputElement) {
fn(event.target.value, event);
}
};
}
// 使用例
document.getElementById('search')?.addEventListener(
'input',
inputHandler((value) => console.log('search:', value))
);
修正の確認方法
- キャストまたはジェネリックパラメータを適用した瞬間に、エディターの赤い波線が消えます。
tsc --noEmitを実行して、そのファイルのエラーがゼロになることを確認します。- IDE で
targetにカーソルを合わせます。ツールチップに汎用的なEventTargetではなく、HTMLInputElement(または宣言した具体的な型)が表示されるはずです。 - ブラウザーの DevTools を開いて、実行時に値が正しくログ出力されることを確認します。
どの修正方法を使うべきか
- 手軽な単発ハンドラー:型アサーション(
as HTMLInputElement) - React フォーム:ジェネリックなイベント型(
React.ChangeEvent<HTMLInputElement>) - 特定の要素へのハンドラー:
currentTarget— キャスト不要 - 共有・ポリモーフィックなハンドラー:
instanceofガード
予防策
まだ設定していなければ、tsconfig.json で strict モードを有効にしましょう。このクラスのエラー全体を、実行時に現れる前に早期に検出してくれます:
{
"compilerOptions": {
"strict": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"]
}
}
lib 配列に "DOM" が含まれているかダブルチェックしてください。これを省略すると TypeScript は HTMLInputElement を認識できなくなり、別のさらに分かりにくいエラーが出て、泥沼にはまることになります。
身につけておきたい習慣があります。addEventListener からの推論に頼るのではなく、イベントハンドラーのパラメーターを明示的に型付けすることです。宣言時に型を厳密にするほど、コンパイラがミスをブラウザーに届く前に早期に検出してくれます。

