타입스크립트에서 named type을 정의하는 방법은 두 가지가 있습니다. 대부분의 경우에는 타입을 사용해도 되고 인터페이스를 사용해도 됩니다. 그러나 타입과 인터페이스 사이에 존재하는 차이를 분명하게 알고, 같은 상황에서는 동일한 방ㅂ버으로 명명된 타입을 정의해 일관성을 유지해야 합니다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
type과 interface의 비슷한 점
1. 인덱스 시그니처는 인터페이스와 타입에서 모두 사용할 수 있다.
type TDict = { [key:string]: string};
interface IDict {
[key:string]: string;
}
2. 함수 타입은 인터페이스와 타입으로 정의 가능하다.
type TFunction = (x: string) => string;
interface IFunction {
(x: string): string;
}
const toStrType: TFunction = x => '@' + x;
console.log(toStrType("hello"));
const toStrInterface: IFunction = x => '@' + x;
console.log(toStrInterface("hello"));
3. Generic 사용이 둘 다 가능하다.
type TPair<T> = {
x: T;
y: T;
}
const numberTPair: TPair<number> = {
x:10,
y:20,
}
console.log(numberTPair.x + " " + numberTPair.y);
interface IPair<T> {
x: T;
y: T;
}
const numberIPair: IPair<number> = {
x:20,
y:30,
}
console.log(numberIPair.x + " " + numberIPair.y);
4. 인터페이스는 타입을 확장할 수 있고, 타입은 인터페이스를 확장할 수 있다.
여기서 주의할 점은 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다는 것입니다. 복잡한 타입을 확장하려면 type과 &를 사용해야 합니다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
// 두 코드는 동일한 기능을 합니다.
type TStateWithPop = IState & { population: number};
interface IStateWithPop extends TState {
population: number;
}
5. 클래스를 구현(implements)할 때는, 타입과 인터페이스 둘 다 가능하다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
class StateT implements TState {
name: string = "";
capital: string = "";
}
class StateI implements IState {
name: string = "";
capital: string = "";
}
type과 interface의 다른 점
1. 유니온 타입은 있지만, 유니온 인터페이스는 없다.
인터페이스는 타입을 확장할 수 있지만, 유니온은 할 수 없습니다. 그런데 유니온 타입을 확장하는 게 필요할 때가 있습니다. 아래와 같은 예시를 보면, 유니온 타입의 장점이 확연하게 드러납니다.
type RequestState = 'idle' | 'loading' | 'success' | 'error';
type RequestData<T> = {
state: RequestState;
data?: T;
error?: string;
}
interface User {
id: number;
name: string;
}
const userRequest: RequestData<User> = {
state: 'success',
data: {id:1, name: "John"}
};
const failedRequest: RequestData<User> = {
state: 'error',
error: "Failed to fetch user"
}
인터페이스로 구현해면 되지 않냐구요? 인터페이스로 구현하고, 타입을 사용했을 경우와 비교해보도록 하겠습니다. 한 눈에 봐도 복잡한 코드임을 알 수 있습니다. 이런 경우에는 타입스크립트의 type을 최대한 활용하는 것이 적절해 보입니다.
interface RequestStateMap {
readonly IDLE: 'idle';
readonly LOADING: 'loading';
readonly SUCCESS: 'success';
readonly ERROR: 'error';
}
const RequestState: RequestStateMap = {
IDLE: 'idle',
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error'
} as const;
interface RequestData<T> {
state: RequestStateMap[keyof RequestStateMap];
data?: T;
error?: string;
}
interface User {
id: number;
name: string;
}
const userRequest: RequestData<User> = {
state: 'success',
data: {id:1, name: "John"}
};
const failedRequest: RequestData<User> = {
state: 'error',
error: "Failed to fetch user"
}
2. type에는 없지만, interface에는 있는 선언 병합(declaration merging)
interface IState {
name: string;
address: string;
}
interface IState {
population: number;
}
// 정상
const mergedInterface: IState = {
name: "Kim",
address: "Seoul",
population: 500_000,
}
// Duplicate identifier 'TState'.
// type TState = {
// name: string;
// address: string;
// }
// type TState = {
// population: number;
// }
참고 자료
- 이펙티브 타입스크립트
- 타입스크립트 GitBook