[TS] 타입별칭과 인터페이스
🔆 처음에는 JS로 충분할 거 같은데 굳이 TS를 배워야 하나? 타입까지 일일이 신경 쓰려면 손도 많이 가고 복잡할 거 같은데...라고 생각했는데 TS 공부를 시작하고 다들 이래서 TS를 쓰는구나 느꼈습니다. 특히 협업을 하거나 천 줄이 넘는 코드를 볼 상황이라면 더욱 TS로 짜인 코드가 오히려 읽기 수월하겠구나 싶었어요.
공부를 하면서 타입 별칭과 인터페이스 간에 유사한 점이 많더라구요.
어떤 걸 사용하면 좋을지 두 개를 비교하면서 정리해 봤습니다!
타입스크립트를 이용하는 이유
타입스크립트는 이름 그대로 타입을 엄격하게 검사해 주는 역할을 해요.
그리고 이건 엄격한 타입체크로 생긴 부가 기능인데 TS를 사용하게 되면 에러 메시지의 퀄리티가 올라가게 돼요.
자바스크립트만 사용했을때는 에러 메시지가 다소 추상적이다는 느낌을 받은 적이 있었을 거예요.
그래서 어디가 문제인지 정확히 알려주지 않는 경우가 많은데 타입스크립트를 사용하면 정확하게 오타 부분을 가리켜주고 정정도 해줍니다.
일종의 JS 헬퍼 역할을 한다고 생각하시면 좋을 거 같아요.
우선 사용자 정의 타입(Type Alias = 타입 별칭)과 인터페이스에 대해 간단히 살펴 볼게요.
타입 별칭(Type Alias)과 인터페이스의 유사점
JavaScript를 사용하면서 객체에 접근해야 하는 상황이 많죠?
이때 객체의 타입을 체크해 주는 역할을 하는 게 타입과 인터페이스입니다.
즉, TypeScript에서 객체 타입을 선언할 때 interface와 type을 사용할 수 있어요.
다르게 말하면 명명된 타입(named type)을 정의하는 방법에 위의 두 가지가 있다고 말할 수 도 있겠네요.
아래의 예시를 보면 각각 type과 interface 방식으로 타입을 명명한 것을 볼 수 있습니다.
// type
type TState = {
name: string;
capital: string;
}
// interface
interface IState {
name: string;
capital: string;
}
상당히 유사하죠?? 이 외에도 비슷한 점이 많아요.
1. 만약 TState와 IState를 추가 속성과 함께 할당한다면 동일한 오류가 발생합니다.
아래 예시를 보면 개체 리터럴은 알려진 속성만 지정할 수 있으며, 위의 타입에는 population이라는 속성이 없다는 에러를 똑같이 발생시킵니다.
2. 함수 타입을 정의할 때도 인터페이스나 타입 별칭을 사용할 수 있습니다.
// type
type TFn = (x: number) => string;
// interface
interface IFn {
(x: number): string;
}
const toStrT: TFn = x => '' + x; // 정상
const toStrI: IFn = x => '' + x; // 정상
이렇게 간단한 타입의 경우에는 타입 별칭을 사용하는 게 더 좋을 수 있겠지만 함수 타입에 추가적인 속성이 있다면 타입이나 인터페이스 어떤 것을 선택하든 차이가 없어요.
아래 예시는 추가적인 속성 prop이 있는 경우입니다.
// type
type TFnWithProperties = {
(x: number): number;
prop: string;
}
// interface
type IFnWithProperties = {
(x: number): number;
prop: string;
}
타입 별칭을 사용하던 인터페이스를 사용하던 비슷하죠?
저는 아직 TS 문법이 퍽 익숙하지는 않더라고요. 앞에서 타입 별칭과 인터페이스는 객체 타입을 선언할 때 사용한다고 했는데 함수의 타입을 선언할 때도 사용을 하네? 뭐지? 🤔🤔🤔
그래서 이펙티브 타입스크립트 책을 보니까 "함수는 호출 가능한 객체라는 것을 떠올려 보면 납득할 수 있는 코드입니다."라는 문구가 있더라고요. 타입스크립트나 자바스크립트나 함수는 일급 객체로 취급돼서 함수가 변수에 할당되거나, 다른 함수의 매개변수로 전달되거나, 반환값으로 사용될 수 있다는 것을 덕분에 다시 정리하고 넘어갔습니다.
3. 타입 별칭과 인터페이스 모두 제너릭이 사용가능합니다.
저는 제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미합니다.라는 글을 TS 공홈에서 보고 제너릭을 쉽게 이해할 수 있었습니다.
예를 들어보면
// 파라미터 text는 어떤 걸 받아도 다 return 해줍니다.
function getText(text) {
return text;
}
getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true
// 비슷하게 제너릭도 생각해 보면 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있습니다.
function getText<T>(text: T): T {
return text;
}
getText<string>('hi'); // string type을 넘겨 줬으니 제너릭의 타입이 <T> = <string> 이 됩니다.
getText<number>(10);
getText<boolean>(true);
제너릭 설명이 길어졌는데 어쨌든 타입 별칭과 인터페이스 모두 제너릭 사용이 가능해요
// type
type TPair<T> = {
first: T;
second: T;
}
// interface
type IPair<T> = {
first: I;
second: I;
}
4. 이 외에도 인터페이스는 extends를 이용해서 type을 확장 가능하고, 타입은 인터페이스를 확장할 수 있습니다.
그리고 둘 다 클래스를 구현(implements)할 수 있어요.
// IStateWithPop과 TStateWithPop은 동일합니다.
// interface의 타입 확장
interface IStateWithPop extends {
population: number;
}
// type의 interface 확장
type TStateWithPop = IState & { population: number };
// 클래스 구현(implements)
// type
class StateT implements TState {
name: string = '';
capital: string = '';
}
// interface
class StateI implements IState {
name: string = '';
capital: string = '';
}
타입 별칭(Type Alias)과 인터페이스의 차이점
타입 별칭과 인터페이스의 가장 큰 차이점은 타입의 병합 가능 / 불가능 여부예요.
인터페이스는 병합이 가능한데 반해 타입 별칭은 확장이 불가능해요.
타입 별칭은 인터페이스와 달리 병합되지 않습니다. 같은 이름의 타입 별칭이 여러 곳에서 선언되더라도, TypeScript는 해당 이름의 타입 별칭을 덮어씌우지 않고 에러를 발생시킵니다.
간단히 말하면, 인터페이스는 여러 개를 합쳐서 하나의 인터페이스로 만들 수 있고, 병합된 인터페이스는 확장되어 기능이 추가됩니다. 하지만 타입 별칭은 병합되지 않으며 독립적인 타입을 형성합니다.
아래에 인터페이스를 이용해서 선언 병합해 속성을 확장시키는 예시를 볼게요!
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}; // 정상
타입 별칭(Type Alias)과 인터페이스 중 어느 것을 사용하며 좋을까?
타입스크립트 공홈에 보면 좋은 소프트웨어는 언제나 확장이 용이해야 한다는 원칙에 따라 가급적 확장 가능한 인터페이스로 선언하는 것이 좋다고 나와 있네용. 하지만 꼭 그런건 아닙니다.
인터페이스를 사용할 경우 병합이 가능하므로 프로퍼티가 추가되는 것을 원치 않다면 타입 별칭을 사용하는 것이 좋아요.
그리고 만약 복잡한 형태의 타입이라면 고민할 것도 없이 타입 별칭을 사용하는 것이 좋다고 합니다. 여기서 복잡한 형태의 타입이라는 것은 type 키워드는 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에도 활용해요. 튜플과 배열 타입도 type 키워드를 사용하면 더 간결하게 사용가능해요. 그러니 복잡한 형태의 타입이라면 타입 별칭을 사용하는 것이 좋겠죠?
이펙티브 타입스크립트 책 내용도 같이 보면 좋을 거 같아서 가져왔습니다.
무엇을 사용할지는 진행 중인 프로젝트에 따라 결정될 수 도 있는데 일관되게 인터페이스를 쓰고 있는 코드베이스에서 작업하고 있다면 인터페이스를 사용하는 것이 좋고, 타입 별칭을 일관되게 사용하고 있다면 타입 별칭을 쓰는 것이 좋다고 합니다.
하지만 아직 스타일이 확립되지 않은 프로젝트라면, 향후 보강의 가능성이 있을지 생각해 봐야 한다고 해요.
어떤 API에 대한 타입 선언을 작성해야 한다면 API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있기 때문에 인터페이스를 사용하는 것이 좋고 하지만 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계이므로 이럴 때는 타입을 사용하는 것이 좋다고 합니다.
결론은 인터페이스를 사용하는 것이 좋기는 하지만 진행 중인 프로젝트 상황과 어디에 쓰일 타입인지에 따라서는 타입 별칭을 쓰는 것이 더 유리할 수 있다인 것 같습니다. 적절히 잘 사용하면 될 것 같아요.
그냥 인터페이스만 써! 하면 좋겠지만 그게 아니어서 조금 어렵네요.
코드 짜보면서 무엇을 선택하면 좋을 지에 대한 안목을 길러야 할 것 같아요.