타입 단언(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를 이용해 타입 단언을 할 수 있었다. 하지만 다른 사람이 이 코드를 보고 한 번에 이해할 수 있을지는 잘 모르겠다.
  • LongContentShortContent는 실제로는 공유하는 필드가 많아 이렇게 선언하게 되었지만, 아예 필드를 따로 분리해도 괜찮을 것 같다. 아마 변경하게 된다면 아래와 같은 모습이 될 것 같다.
interface Section { title: string; type: 'long' | 'short'; longContent: LongContent[]; shortContent: ShortContent[]; }

참고