
TypeScript를 쓰다 보면 type과 interface의 차이에 대해 (저만) 깊이 고민하지 않고 습관처럼 interface만 주로 쓰게 되는 경우가 있었는데요 😳. 어렴풋하게 알고 있던 부분들을 정리해보며 다시 한 번 개념을 정리해보았어요!
type과interface 모두 객체 타입을 선언할 수 있는 문법이에요.
React 컴포넌트의 Props 타입을 정의할 때도 자유롭게 사용할 수 있어요.

interface는 extends 키워드를 사용해 다른 인터페이스를 명시적으로 상속할 수 있어요.type은 & 연산자를 활용해 교차 타입(intersection)으로 조합할 수 있어요.예를 들어 여러 컴포넌트에서 공통으로 쓰이는 Props를 확장해서 재사용하고 싶을 때 두 방식 모두 사용할 수 있는데요.

CommonProps와 같이 여러 컴포넌트에서 공통으로 사용하는 속성을 정의하고 ButtonProps와 같이 개별 컴포넌트의 고유 속성을 덧붙이는 계층적 구조에서는 interface의 extends 키워드를 사용하는 방식이 더 직관적이고 선언적으로 읽혀요.

type의 & 연산자를 통해 동일한 타입 구조를 만들 수는 있지만 조합이 중첩될수록 타입 간의 관계가 복잡해지고 전체 구조나 의도를 직관적으로 파악하기 어려워질 수 있어요!
특히 타입 계층이 깊어지거나 재사용되는 타입이 많아지는 경우, interface의 extends를 사용하는 방식이 계층 구조를 더 명확하게 표현할 수 있어 가독성과 유지보수 측면에서 유리합니다.
type은 객체 구조를 정의하는 것뿐 아니라 유니온 타입, 튜플, 리터럴 타입 등 다양한 타입 표현을 지원해요. 반면 interface 이러한 표현을 직접적으로 지원하지 않기 때문에 않기 때문에, 값의 제약이나 복잡한 타입 조합이 필요한 상황에서는 type을 사용하는 편이 더 유연해요.

💡
interface에서도 개별 속성에 유니온 타입을 지정할 수 있어요!
예를 들어 버튼의 variant처럼 특정 문자열 값 중 하나를 선택해야 하는 경우 다음과 같이 정의할 수 있습니다
interface자체를 유니온 타입으로 선언하는 것은 불가능하기 때문에 타입 전체가 유니온 형태를 가져야 하는 경우에는type을 사용하는 것이 적절해요.
interface는 동일한 이름으로 여러 번 선언하면 자동으로 병합되어 하나의 타입처럼 동작합니다.type은 동일한 이름으로 중복 선언할 경우 컴파일 에러가 발생합니다
이런 특성 덕분에 interface는 전역 객체 확장이나 외부 라이브러리의 타입 보강에 자주 사용됩니다.
예를 들어 window 객체에 커스텀 전역 속성을 추가하고자 할 때 아래와 같이 Window 인터페이스를 확장해 타입을 정의할 수 있어요.

이렇게 선언해두면 코드에서 window.__APP_VERSION__을 사용할 때도 타입이 안전하게 보장되며,
보통 이러한 선언은 custom.d.ts와 같은 타입 정의 파일에 별도로 작성합니다.

이와 같이 type을 사용하여 기존 타입과 교차 타입(&)으로 조합하는 것은 문법적으로는 가능합니다.
그러나 전역 객체 확장이나 외부 선언과 같이 컴파일러가 타입 병합을 자동으로 수행해야 하는 상황에서는 이 방식이 동작하지 않아요.
또한 이 방식은 실제 코드에서 window as MyWindow와 같이 명시적인 타입 단언(type assertion)을 반복적으로 작성해야 하는 번거로움이 있습니다.
🔎 타입 단언은 컴파일러에게 “이 값은 내가 보장하는 타입이야”라고 명확히 알려주는 방식으로 런타임에서 실제 값을 변경하지는 않지만 타입 검사의 신뢰를 우회하게 됩니다. 잘못 사용하면 런타임 오류로 이어질 수 있기 때문에 주의가 필요해요. 🧐
다시 정리하면,type의 & 연산자는 타입을 조합하는 수동적인 도구이고 interface의 선언 병합은 타입 시스템 내부에서 구조를 능동적으로 확장하는 메커니으로 이해하면 좋아요.
따라서 전역 객체 확장, 외부 라이브러리 타입 보강 등 자동 병합이 필요한 환경에서는 interface를 사용하는 것이 적절합니다.
type과 interface 모두 함수 타입 오버로딩을 지원하기 때문에
입력값의 타입에 따라 다른 동작을 하는 함수도 두 방식으로 모두 정의할 수 있어요.
예를 들어 셀렉트 박스나 드롭다운 컴포넌트에서 선택된 값(value)으로부터 해당 옵션의 label을 찾아주는 유틸리티 함수를 작성한다고 가정해 볼 수 있어요.

interface를 사용하면 입력값의 타입별로 시그니처를 나열할 수 있어서 인자의 타입에 따라 어떤 결과가 반환되는지 명확하게 파악할 수 있어요. 추후 오버로드가 늘어날 가능성을 생각한다면, interface 방식이 구조적으로 더 명확하고 확장에도 유리한 선택이 될 수 있어요.

type을 사용해도 오버로딩된 함수 타입을 동일하게 정의할 수 있어요. 다만 중괄호 안에 시그니처를 나열하는 형태다 보니 객체 타입처럼 보일 수 있고 실제 기능상의 차이는 없지만 팀마다 가독성이나 스타일에 대한 선호는 다를 수 있어요.

물론 이 방식도 동작에는 문제가 없지만 각 타입별로 어떤 동작을 하는지 명확하게 구분되지 않다 보니 타입 추론이 조금 흐려질 수 있어요. 특히 내부 로직에 분기 처리가 많아지는 경우라면, 명시적으로 시그니처를 나열해주는 방식이 더 명확하게 의도를 드러낼 수 있답니다.
TypeScript 공식 문서에서도 interface와 type 중 대부분은 개인 선호에 따라 선택할 수 있지만 기준이 필요하다면 기본적으로는 interface를 사용하고, 유니온 타입이나 튜플 등 type만의 기능이 필요한 경우 type을 쓰는 방식을 권장하고 있어요.
이번 정리를 통해 type과 interface는 기능적으로는 비슷하지만 내가 정의하려는 타입의 구조나 역할, 그리고 앞으로 확장 가능성이 있는지 등을 함께 고려해 목적에 맞게 유연하게 선택하는 것이 중요하다는 점을 다시 한 번 느꼈어요.
이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻