자바스크립트에서 이벤트리스너를 사용할때 e.target.innerText
를 사용한 적이 있던 기억이 있었기에 타입스크립트에서 아래와 같은 코드가 정상작동하리라 생각했다.
function App() {
const clickHandler = (e: MouseEvent<HTMLElement>) => {
const target = e.target.innerText;
};
return (
<div>
App Component
</div>
)
}
위 코드에서는 innerText
부분에서 에러가 발생했는데 이유는 e.target
의 타입이 EventTarget
인데 EventTarget
타입은 innerText
를 갖지 않고 있기 때문이다.
그래서 as
단언 방식으로 타입스크립트 컴파일러가 e.target
을 HTMLElement
로 보게했다.
왜냐하면 HTMLElement
타입에는 innerText
속성을 가지고 있기 때문이다.
function App() {
const clickHandler = (e: MouseEvent<HTMLElement>) => {
const target = e.target as HTMLElement;
const text = target.innerText;
};
return (
<div>
App Component
</div>
)
}
위 코드는 정상작동했다. 하지만 as
를 쓰는게 꺼림칙했고 이렇게 innerText
에 접근하는 것이 옳은 것인지 의문도 들었다. 그래서 React
의 타입 선언 파일을 확인하여 MouseEvent
타입에 대해 알아보고자 했다.
MouseEvent
타입은 아래와 같이 서브 타이핑을 구현하고 있었다.
interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {}
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {}
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
interface BaseSyntheticEvent<E = object, C = any, T = any> {
nativeEvent: E;
currentTarget: C;
target: T;
}
위 코드의 마지막 인터페이스인 BaseSyntheticEvent
를 보면 제네릭 타입 C
가 EventTarget & T
임을 알 수 있다.
interface BaseSyntheticEvent<E = object, EventTarget & T, T = any> {
nativeEvent: E;
currentTarget: EventTarget & T;
target: T;
}
서브 타입에서 들어오는 타입 인자를 제네릭 C
에 풀어 써보면 위와 같이 된다.
더 풀어 써보면
interface BaseSyntheticEvent<E = object, EventTarget & HTMLElement, T = any> {
nativeEvent: E;
currentTarget: EventTarget & HTMLElement;
target: T;
}
위의 형태가 됨을 알 수 있다.
여기까지 와서 EventTarget & HTMLElement
교차타입 연산이 어떻게 결과가 나오는지 생각해봐야 했다.
// node_modules/typescript/lib/lib.dom.d.ts
interface EventTarget {
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
dispatchEvent(event: Event): boolean;
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}
interface HTMLElement extends Element, DocumentAndElementEventHandlers, ElementCSSInlineStyle, ElementContentEditable, GlobalEventHandlers, HTMLOrSVGElement {
accessKey: string;
readonly accessKeyLabel: string;
autocapitalize: string;
dir: string;
draggable: boolean;
hidden: boolean;
inert: boolean;
innerText: string;
lang: string;
readonly offsetHeight: number;
readonly offsetLeft: number;
readonly offsetParent: Element | null;
readonly offsetTop: number;
readonly offsetWidth: number;
outerText: string;
spellcheck: boolean;
title: string;
translate: boolean;
attachInternals(): ElementInternals;
click(): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
타입스크립트에서 교차타입은 합집합이므로
두 타입의 교차타입이 innerText
속성을 가지고 있음을 알 수 있다.
function App() {
const clickHandler = (e: MouseEvent<HTMLElement>) => {
const text = e.currentTarget.innerText;
};
return (
<div>
App Component
</div>
)
}