为什么在TypeScript中Event.target不是Element?

243

我只想用我的KeyboardEvent来做这件事

var tag = evt.target.tagName.toLowerCase();

虽然 Event.target 的类型是 EventTarget,但它并不继承自 Element。因此我需要像这样进行强制转换:

var tag = (<Element>evt.target).tagName.toLowerCase();

这可能是因为一些浏览器没有遵循标准,对吗?在TypeScript中,什么是正确的跨浏览器实现方式?

P.S. 我正在使用jQuery捕获KeyboardEvent


17
更简洁的语法:var element = ev.target as HTMLElement 的意思是将 ev.target 转换成 HTMLElement 类型并赋值给变量 element - Adrian Moisa
12个回答

157

JLRishe的回答是正确的,因此我在事件处理程序中只是使用了这个:

if (event.target instanceof Element) { /*...*/ }

6
这是最安全的解决方案! - Igor Golopolosov
这实际上是最好的解决方案,因为它在运行时实际检查目标。 - 3Dos
完美解决方案并应标记为答案。我在使用Svelte框架时添加svelte:window单击处理程序时正在寻找一个好的解决方案,而这个问题得到了解决。 - Jason Holtzen
请注意,使用这种方式,如果事件在非元素目标上触发,则不会调用侦听器。例如:document.body.firstChild.click()(第一个子节点通常是空的文本节点)。 - fregante
我可以用 HTMLElement 代替 Element 吗? - redanimalwar

130

它没有继承自Element,因为并非所有的事件目标都是元素。

来自MDN

元素、文档和窗口是最常见的事件目标,但其他对象也可以成为事件目标,例如XMLHttpRequest、AudioNode、AudioContext等。

即使是您正在尝试使用的KeyboardEvent也可能发生在DOM元素或窗口对象上(理论上还可能发生在其他物体上),因此,在这种情况下,将evt.target定义为Element就没有意义了。

如果它是DOM元素上的事件,那么我认为您可以安全地假设evt.target是一个Element。 我认为这不是跨浏览器行为问题,仅仅是因为EventTarget是一个比Element更抽象的接口。

更多阅读:https://github.com/Microsoft/TypeScript/issues/29540


18
在这种情况下,KeyboardEvent和MouseEvent应该有自己的EventTarget等效项,它将始终包含相关的元素。DOM非常不可靠...:/ - daniel.sedlacek
9
我不是DOM或TypeScript方面的专家,但我认为EventTarget的设计存在太多歧义,这与TypeScript无关。 - daniel.sedlacek
4
另一方面,KeyboardEvent 可以在 DOM 元素和窗口对象上(理论上也可能在其他对象上)触发,因此很难将 KeyboardEvent.target 的类型限定得比 EventTarget 更具体。除非您认为 KeyboardEvent 也应该是一个泛型类型 KeyboardEvent<T extends EventTarget>,并且想在代码中到处都写 KeyboardEvent<Element>。但那时,您最好直接进行显式转换,即使这样会很麻烦。 - JLRishe
18
如果将来有人有类似的需要,以下内容可能会有所帮助:我需要将一个变量指定为特定的元素类型,以便访问<select>标签的value属性。例如:let target = <HTMLSelectElement> evt.target; - munsellj
2
@munsellj(不幸的是),这是在类型环境中处理歧义的正确方式。 - pilau
显示剩余4条评论

81
使用TypeScript,我使用自定义接口,只适用于我的函数。示例用例。
  handleChange(event: { target: HTMLInputElement; }) {
    this.setState({ value: event.target.value });
  }

在这种情况下,handleChange 函数将接收一个包含 type 为 HTMLInputElement 的 target 字段的对象。

稍后在我的代码中,我可以使用

<input type='text' value={this.state.value} onChange={this.handleChange} />

更为清晰的方法是将接口放到一个单独的文件中。

interface HandleNameChangeInterface {
  target: HTMLInputElement;
}

然后之后使用以下函数定义:

  handleChange(event: HandleNameChangeInterface) {
    this.setState({ value: event.target.value });
  }
在我的使用场景中,明确规定只有输入框的 HTML 元素类型才是 handleChange 的唯一调用方。

这对我来说完美地运行了 - 我尝试了各种恶心的方法,扩展了EventTarget等等,但这是最干净的解决方案 +1 - Kitson
5
补充一下,如果你需要扩展事件定义,你可以这样做:handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement> & { target: HTMLInputElement }) => {...}。意思是在React中定义一个具有扩展事件的函数,其中包含键盘事件和目标输入元素。 - Kitson
1
自定义接口会破坏有关事件对象的其余信息。@Kitson的示例是附加的,而且更好。 - johnny

61

Typescript 3.2.4

要检索属性,您必须将目标转换为相应的数据类型:

e => console.log((e.target as Element).id)

这与<HTMLInputElement>event.target;语法相同吗? - Konrad Viltersten
@KonradViltersten,它们做相同的事情。 as 语法是为了避免与 JSX 冲突而引入的。建议使用 as 以保持一致性。https://basarat.gitbooks.io/typescript/docs/types/type-assertion.html - Adam
啊哈,我明白了。它看起来更像C#,在许多情况下这是一个优势,取决于团队的后端经验。只要它不是那些假朋友之一,其中语法类似于某些东西,但在技术上意味着完全不同的东西。(我在想Angular和C#之间的var和const,这是我的悲惨经历,呵呵)。 - Konrad Viltersten
@KonradViltersten 关于“C#的外观”,也许是因为这两种语言都是微软的,而且一个人在开发中扮演了重要角色,即Anders Hejlsberg作为C#的首席架构师和TypeScript的核心开发人员。尽管我同意,在非常相似的语言中使用相同的语法表示不同的含义并不好。 https://zh.wikipedia.org/wiki/Anders_Hejlsberg - Bangonkali
3
如果你不想要一个不可靠的代码库,使用as应该是最后的选择。 - johnny

22

您能否创建自己的通用界面来扩展Event。类似于这样?

interface DOMEvent<T extends EventTarget> extends Event {
  readonly target: T
}

然后您可以这样使用它:
handleChange(event: DOMEvent<HTMLInputElement>) {
  this.setState({ value: event.target.value });
}

1
哦,很高兴你还在维护它,谢谢 :) 虽然那里并不需要 readonly,但多一点类型安全也无妨(我希望没有人会尝试覆盖 target 属性,但你永远不知道,我见过更糟糕的情况)。 - Oleg Valter is with Ukraine
type DOMEvent<E extends Event, T extends Element> = E & { readonly target: T; };这是上面变化的一个版本,它不会破坏原始事件类型。例如,DOMEvent<TouchEvent, HTMLButtonElement>仍然可以正确自动完成event.touches。感觉很遗憾,Event无法从处理程序绑定到的目标推断出target类型。 - johnny
请参考此处的 React.MouseEvent<HTMLDivElement> 等内容链接链接 - milahu

2

使用TypeScript,我们可以利用类型别名,像这样:

type KeyboardEvent = {
  target: HTMLInputElement,
  key: string,
};
const onKeyPress = (e: KeyboardEvent) => {
  if ('Enter' === e.key) { // Enter keyboard was pressed!
    submit(e.target.value);
    e.target.value = '';
    return;
  }
  // continue handle onKeyPress input events...
};

2

我使用这个:

onClick({ target }: MouseEvent) => {
    const targetElement: HTMLElement = target as HTMLElement;
    
    const listFullHeight: number = targetElement.scrollHeight;
    const listVisibleHeight: number = targetElement.offsetHeight;
    const listTopScroll: number = targetElement.scrollTop;
    }

1
如果他们不点击<div>会怎么样? - wheeleruniverse
1
对于一个简单的按钮点击事件,我愿意使用这种方法,以避免使用其他方法时的混乱和冗长的代码。 - undefined

2
对于Angular 10+用户:
只需声明HTML输入元素并将其扩展为使用目标作为对象,就像我在我的Bootstrap 4+文件浏览器输入下面所做的那样。这样你就可以节省很多工作。
  selectFile(event: Event & { target: HTMLInputElement}) {
    console.log(event.target.files);
    this.selectedFile = event.target.files[0];
  }

1
我不确定 Angular 中的事件类型,但你的事件应该是某种事件类型,而不是 HTMLInputElement。 - Kaz
这种技术似乎只有在tsconfig.json文件中的angularCompilerOptions中关闭strictTemplates时才有效。 - billoreid

1

@Bangonkali提供了正确的答案,但是这个语法对我来说更易读,而且更加美观:

eventChange($event: KeyboardEvent): void {
    (<HTMLInputElement>$event.target).value;
}

1

正如JLRishe在他的回答中所正确说的,Event对象的目标字段不一定是一个Element。然而,我们可以确定,任何继承自UIEvent的事件在监听器中始终会有一个target字段,该字段的类型实现了Node接口,只有Windows这种情况除外。我将以这种方式介绍这个异常(使用点击事件的示例):

window.addEventListener('click', e => {
  console.log(e.target)                      // Window
  console.log(e.target instanceof Node)      // false
}
window.dispatchEvent(new MouseEvent('click'))

在所有其他情况下,它将是从Node继承的任何类型。
为什么是Node而不是Element?
那是个很好的问题。可以公正地说,如果事件是由用户与GUI的交互本地触发的,那么它将始终是实现Element的一种类型。但仍然有可能人为地调用任何UIEvent:
const node = document.createTextNode('')
document.body.appendChild(node)
window.addEventListener('click', e => {
    console.log(e.target instanceof Element))    // false
}
node.dispatchEvent(new MouseEvent('click', {bubbles: true}))

虽然这个例子是关于球形真空的,但它是可能的。

所有这些都非常有趣,但也很困难。这就是为什么我建议使用一个,它可以处理所有可能的情况并自动输出安全类型。这个包就是types-spring。为了清晰起见:

listener 是 HTMLElement

原始类型:
const elem = document.querySelector('div')
if (elem) elem.addEventListener('click', e => {

    e.target                               // EventTarget| null
    if (e.currentTarget)
    {
         e.currentTarget                   // EventTarget
    }
})
有类型-春天:
const elem = document.querySelector('div')
if (elem) elem.addEventListener('click', e => {

    e.target                               // Node | null
    if (e.currentTarget)
    {
         e.currentTarget                   // HTMLDivElement 
    }
})

监听器是窗口

原始类型:
window.addEventListener('click', e => {
    if (e.target) {
        if (e.isTrusted === true) {
            e.target                       // is EventTarget
        }

        if (e.target instanceof Window) {
            e.target                       // is Window
        }
        else {
            e.target                       // is EventTarget
        }
    }
})
使用spring类型:

window.addEventListener('click', e => {
    if (e.target) {
        if (e.isTrusted === true) { 
            e.target                       // is Element
        }

        if ('atob' in e.target) {
            e.target                       // is Window
        }
        else {  
            e.target                       // is Node
        }        
    }
})

希望这个包能够帮助日常开发。
附言:对于这个有什么想法吗?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接