들어가며
타입스크립트에서 타입을 만들 때 보통 두 가지 방법을 사용합니다.
// type을 사용하는 방법
type SomeType = {
name: string;
figure: number;
};
// interface를 사용하는 방법
interface SomeInterface {
name: string;
figure: number;
}
둘의 동작은 모두 동일해 보입니다.
const Foo: SomeType = {
name: 'foo',
figure: 1,
};
const Boo: SomeInterface = {
name: 'boo',
figure: 2,
};
둘 모두 타입체커를 정상적으로 통과합니다.
여기서 “타입스크립트에서는 ‘어떤 이유’가 있어서 분명히 두 개를 나누어 만들었을 텐데 그게 무엇 때문일까?” “어떤 상황에서 어떤 걸 써야하지?” 와 같은 의문이 생기게 됩니다. 공식 문서에서는 다음과 같이 말하고 있습니다.
대부분의 경우 개인적 선호에 따라 인터페이스와 타입 중에서 선택할 수 있으며, 필요하다면 TypeScript가 다른 선택을 제안할 것입니다. 잘 모르겠다면, 우선
interface
를 사용하고 이후 문제가 발생하였을 때type
을 사용하기 바랍니(참고: typescriptlang.org/ko/docs/handbook/2/everyday-types.html)
처음에는 이 내용을 토대로 interface
를 위주로 사용하되 필요한 경우에 type
을 사용했습니다. 하지만 타입스크립트에 대해 깊게 알아가고, 다른 라이브러리들의 타입을 살펴보며 이 둘의 명확한 차이를 알아야 코드의 의도를 정확하게 파악할 수 있겠다는 생각이 들었습니다. 따라서 지금부터 이 둘의 차이를 좀 더 명명백백하게 알아보는 시간을 가져보고자 합니다.
type과 interface의 공통점
- type과 interface 모두 인덱스 시그니처를 사용할 수 있습니다.
type Foo = {
[key: string]: string;
};
interface Boo {
[key: string]: string;
}
- 함수 타입 또한 모두 사용할 수 있습니다.
type NumToStringFuncType = (num: number) => string;
interface NumToSTringFuncInterface {
(num: number) => string;
}
- type, interface 모두 제네릭이 가능합니다.
type SameType<T> = {
a: T;
b: T;
};
interface SameInterface<T> {
a: T;
b: T;
}
type과 interface의 차이점
- 유니온 타입
type 에는 type fruits = 'apple' | 'banana'
와 같은 유니온 타입이 있지만, interface에는 없습니다. interface는 type을 확장할 수 있습니다. 하지만 유니온을 확장할 수는 없습니다.
- 원시 타입 별칭
유니온과 비슷한 이야기지만, type은 원시 타입에 대한 타입 선언이 가능하나 interface는 오직 객체의 모양을 선언하는 데에만 사용됩니다.
// type은 아래와 같이 원시 타입 선언이 가능합니다.
type SanitizedString = string;
type EvenNumber = number;
// 아래는 interface로는 불가능한 형태입니다.
interface X extends string {}
이와 같은 형태를 통해 튜플과 같은 형태 또한 type으로 선언하는 것이 유리하다는 판단이 듭니다.
type Tuple = [number, number]
- augment(보강)
위 두 가지 차이점은 type에만 있는 것이라면 보강
개념은 interface에만 존재하는 것입니다. 공식문서에서는 이 차이점을 가장 핵심적이라고 언급합니다.
이 둘의 가장 핵심적인 차이는, 타입은 새 프로퍼티를 추가하도록 개방될 수 없는 반면, 인터페이스의 경우 항상 확장될 수 있다는 점입니다.
interface Country {
name: string;
capital: string;
}
interface Country {
population: number;
}
const Seoul: Country {
name: '대한민국',
capital: '서울',
population: '50_000_000'
};
이런 식으로 속성을 확장하는 것을 선언 병합 declaration merging
이라고 합니다. 이런 방식으로 ES의 새로운 버전이 나오게 되는 등의 업데이트에서는 해당 d.ts
파일에 새로이 선언된 interface를 병합하는 방식으로 우리는 ES 버전에 따른 타입을 얻을 수 있습니다.
이러한 방식으로 type에 적용하게 되면 에러가 나오게 됩니다.
type Country = {
name: string;
capital: string;
};
type Country = {
population: number;
};
// Error: Duplicate identifier 'Country'.
- 상속(혹은 확장)
둘 다 기존 타입을 통해 새로운 타입을 만들 수 있지만, 그 방식에서 작은 차이가 있습니다.
// type의 확장 - 교집합
type Animal = {
name: string;
};
type Bear = Animal & {
honey: Boolean;
};
const bear = getBear();
bear.name;
bear.honey;
// interface의 확장 - extends
interface Animal {
name: string;
}
interface Bear extends Animal {
honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
그래서 뭘 써야하는데요?
- 복잡한 타입
복잡한 타입은 고민할 것도 없이 type
을 사용하면 됩니다. type
은 원시 타입과 유니온을 포함시킬 수 있으며 interface
에서 표현할 수 없는 부분들 또한 표현 할 수 있기 때문입니다.
- 복잡하지 않다면?
만일 type
을 굳이 사용하지 않아도 될 정도로 복잡하지 않은 타입이라면 어떻게 해야 할까요? 이런 경우에는 일관성
과 보강
의 관점으로 바라볼 필요가 있습니다.
우선 일관성의 관점에서는 진행 중인 프로젝트의 컨벤션을 따라가야 한다 생각합니다. 만일 처음 언급했던 공식 문서의 우선 interface
를 사용하고 이후 문제가 발생하였을 때 type
을 사용 이라는 관점에서 컨벤션을 진행시킨다면 복잡하지 않은 케이스에서는 interface
를 사용하면 됩니다. 만약 향후 보강
의 가능성이 농후한 프로젝트라면 interface
의 사용의 필요성이 커지겠죠.
마치며
지금까지 type과 interface의 공통점 및 차이점에 대해 알아보고 상황별 어떤 걸 써야 더 적합한지에 대해 알아봤습니다. 항상 사용하던 타입 키워드들이었지만 그 용도를 자세히 살펴보니 새로운 시각이 트임을 느꼈습니다. 또 타입스크립트의 깊이에 다시금 놀랐습니다. 앞으로 이러한 용도를 상기시키며 정확하고 적합한 타입을 사용할 수 있도록 노력해 보는 것이 어떨까요??