타입 단언(Type Assertion)으로 타입 에러 해결하기
TS보다 내가 타입을 더 잘 알고있는 경우도 있습니다
Table Of Contents
들어가기
- 복잡한 데이터 구조를 수정하다가 타입 에러를 만났다. 어떻게 해결할 수 있을까?
- 데이터 구조상의 문제일 수도 있지만, 여기에서는 타입 단언으로 해결하는 사례를 소개합니다.
타입 단언(Type Assertion)이란?
TS가 추론하기 어려우면서 나(코드 작성자)는 타입을 알고 있는 경우, 내가 직접 타입 단언을 해 줄 수 있다!
예시
링크에서 예시를 확인할 수 있다.
const myCanvas = document.getElementById('main_canvas');
위 코드에서 TS는 myCanvas가 document.getElementById
의 return값을 받고 있기 때문에, 어떤 종류의 HTMLElement
타입이 된다고 추론할 것이다.
하지만 코드 작성자는 main_canvas
id를 가지는 html 요소가 HTMLCanvasElement
타입이라는 것을 알고 있을 수 있다. 이런 경우에는 아래처럼 타입을 직접 지정해줄 수 있다.
const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement;
혹은 아래처럼 꺽쇠 <>
를 사용해도 타입을 단언할 수 있다.
const myCanvas = <HTMLCanvasElement>document.getElementById('main_canvas');
하지만 이 방법은 .tsx 파일에서는 사용할 수 없다!! 꼭 명심하자.
문제상황
간략히 다음과 같은 데이터 형식을 가지고 있다고 생각해보자.
interface LongContent { title: string; description: string[]; } interface ShortContent { title: string; } interface Section { title: string; type: 'long' | 'short'; content: LongContent[] | ShortContent[]; }
Section
이라는 타입 안에, content
라는 필드가 있는데, 그 타입은 LongContent
의 배열 또는 ShortContent
의 배열이다.
이제 Section 타입의 데이터를 수정하려고 한다.
const initialState: Section = { title: '섹션 1', type: 'long', content: [ { title: '내용 1', description: ['설명 1'], }, { title: '내용 2', description: ['설명 2'], }, ], }; const [data, setData] = useState<Section>(initialState);
임의로 데이터를 생성해보았다. setData
를 이용해서 description
에 일괄적으로 새로운 설명을 추가하는 함수는 이렇게 작성할 수 있을 것이다.
setData((prevData: Section) => { return { ...prevData, content: prevData.content.map((content) => { return { ...content, description: content.description.concat('새로운 설명'), }; }), }; });
하지만 이 함수에서는 타입 에러가 발생할 것이다.
당연한 이야기지만, description
이라는 필드는 LongContent
에만 존재하고, ShortContent
타입에는 존재하지 않기 때문이다. content(prevData.content)
는 LongContent[] | ShortContent[]
타입이므로 content(prevData.content)
를 map으로 순회할 때, 개별 아이템은 LongContent| ShortContent
타입일 것이다.
그런데 만약 내가 LongContent[]
타입일 때만 이 함수가 호출된다는 사실을 알고 있다면 어떨까?
LongContent
타입에서는 description
이 존재하기 때문에 해당 오류는 더이상 적용되지 않을 것이다. 하지만 TS가 이런 사실을 추론하기 어렵기 때문에, 사용자가 직접 content(prevData.content)
이 LongContent[]
타입이라고 명시해줄 수 있다.
setData((prevData: Section) => { return { ...prevData, content: (prevData.content as LongContent[]).map((content) => { return { ...content, description: content.description.concat('새로운 설명'), }; }), }; });
이렇게 map으로 순회하기 전에 LongContent[]
타입이라고 명시한다면 해당 에러는 더이상 나타나지 않는다.
생각해보기
- 이번에는 내가 코드 전체를 파악하고 있기 때문에
as
를 이용해 타입 단언을 할 수 있었다. 하지만 다른 사람이 이 코드를 보고 한 번에 이해할 수 있을지는 잘 모르겠다. LongContent
와ShortContent
는 실제로는 공유하는 필드가 많아 이렇게 선언하게 되었지만, 아예 필드를 따로 분리해도 괜찮을 것 같다. 아마 변경하게 된다면 아래와 같은 모습이 될 것 같다.
interface Section { title: string; type: 'long' | 'short'; longContent: LongContent[]; shortContent: ShortContent[]; }
참고
- TypeScript docs - Everyday Types
- TypeScript Deep Dive - 타입 표명(Type Assertion)
- 타입스크립트 교과서(조현영 저)