[TypeScript] 타입(type)과 인터페이스(interface)의 차이점
🍀 목차
확장
새 필드 추가
사용 범위
Index Signature
type으로만 선언 가능한 타입들
타입(type)과 인터페이스(interface)는 TypeScript에서 객체의 타입을 만드는 데 사용된다. type alias로는 객체 타입뿐 아니라 모든 유형의 타입을 만들 수 있는데, 이처럼 두 키워드의 특징과 차이점을 알아보자.
확장
type(교차 타입 사용)
type Animal = {
name: string
sound: string
}
type BearFeature = {
honey: boolean
sound: string
}
type Bear = Animal & BearFeature;
const bear:Bear = {name:"Pooh",sound:"kkao~",honey:true};
bear.name;
bear.honey;
bear.sound;
타입은 &(intersection)
을 활용하여 확장이 가능하다. 집합으로 생각해 보면 타입 Animal, 타입 BearFeature를 모두 만족하는 경우라고 생각하면 이해가 쉽다.
교차 타입(intersection)을 사용하면 여러 타입을 합쳐 하나의 타입으로 사용할 수 있다. type Bear = Animal & BearFeature;
Bear 타입의 모든 값은 Animal 타입의 값이면서 BearFeature 타입의 값이다.
type Parent = {
printName: (name: number) => void;
}
type Child = Parent & {
// (name: number | string) => void 와 같아진다.
printName: (name: string) => void;
}
const test: Child = {
printName: (name: number | string) => {
console.log('name: ' , name);
},
};
test.printName(1);
test.printName('1');
interface(extends 사용)
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {name:'fubao', honey:false};
bear.name;
bear.honey;
인터페이스는 extends 키워드를 사용하여 확장이 가능하다.
interface Parent {
printName: (name: number) => void;
}
// Error. Interface 'Child' incorrectly extends interface 'Parent'.
interface Child extends Parent {
printName: (name: string) => void;
}
// 그대신 아래와 같은 상속이 가능하다.
interface Child extends Parent {
printName: (name: string | number) => void;
}
// 라이브러리를 만들고 open API를 제공하면 사용자가 인터페이스의 해당 특징을 사용하여 확장할 수 있어 편하다.
인터페이스들을 교차 타입으로 만들어 type alias로 선언하는 것도 된다.
// 이런 것이 불가능
interface Animal & BearFeature {
name: string
sound: string
}
// 이런 건 가능
interface Animal {
name: string
sound: string
}
interface BearFeature {
honey: boolean
sound: string
}
type Bear = Animal & BearFeature;
type Foo = Bar & Baz & {
someProp: string;
}
interface Foo extends Bar, Baz {
someProp: string;
}
Interfaces create a single flat object type that detects property conflicts, which are usually important to resolve! Intersections on the other hand just recursively merge properties, and in some cases produce never.
https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections
확장에 있어서 인터페이스는 중복으로 선언된 충돌을 해결하기 위해 단순 객체 타입을 생성한다. 인터페이스는 객체만 정의하기에 합치기만 하면 되지만, 타입의 경우보다 다양한 타입이 오기에 이를 재귀적으로 순환하며 속성을 merge하게 된다. 이 과정에서 호환되지 않는 타입들이 교차되는 등 특정 상황이 발생하여 TypeScript에서 값의 공집합을 뜻하는 never타입이 나올 수도 있다.
type type1 = {a:1; b:2} & {b:3};
이렇게 제대로 merge가 되지 않을 수도 있다. 객체에서만 쓰는 용도라면 인터페이스를 쓰는 것이 좋다.
새 필드 추가
type
type Window = {
title: string
}
type Window = {
ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.
// 병합되지 않는다. 고유해야 한다.
interface
선언적 확장이 가능하다.
interface Window {
title: string
}
interface Window {
ts: TypeScriptAPI
}
const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
편리한 확장이지만 한편으로는 동일 스코프에 동일한 이름의 타입을 중복으로 할당한다는 점이 문제 발생의 원인이 될 수 있으므로 ESLint로 변수 재선언을 허용하지 않는 no-redeclare을 활성화시킬수도 있다(해당 규칙으로 확인된 문제는 TS 컴파일러에서 자동으로 확인되지만 컴파일러 오류 메시지보다 ESLint 오류 메시지를 선호하는 경우에는 활성화시키면 된다).
사용 범위
type
객체 O, 원시타입 O
type AnObject1 = {
value: string
}
// 객체 사용 가능
type SanitizedString = string
type EvenNumber = number
// primitive 타입을 활용하여 사용자 정의 이름 만들기 가능
interface
객체 O, 원시타입 X
interface AnObject1 {
value: string
}
// 객체 사용 가능
interface X extends string {}
// interface는 primitive 타입을 활용한 사용자 정의가 가능하지 않다.
Index Signature
인덱스 시그니처(Index Signature)는 객체 내부에 [Key: T]: U
형식으로 선언하며, 해당 타입의 속성 키는 모두 T, 속성 값은 U를 가져야 함을 의미한다. 객체가 Key, Value의 형식, 각각의 타입을 명확히 해야 하는 경우 사용한다.
타입은 암묵적 인덱스 시그니처를 갖는다.
// KnownAttributes가 type이라면 오류가 나지 않는다.
interface KnownAttributes{
x:number
y:number
}
const knownAttributes: KnownAttributes={
x:1,
y:2
}
type RecordType = Record<string, number>;
//Type 'KnownAttributes' is not assignable to type 'RecordType'.
//Index signature for type 'string' is missing in type 'KnownAttributes'.
const temp: RecordType = knownAttributes;
인터페이스는 추후 확장될 수 있기에 인덱스 시그니처를 활용하려면 명시적으로 정의해줘야 한다.
interface KnownAttributes {
x:number
y:number
[z: number]:string;
}
const knownAttributes: KnownAttributes={
x:1,
y:2,
1:'z'
}
const temp: KnownAttributes = knownAttributes;
마지막으로 타입으로만 가능한 것들을 알아보자.
type으로만 선언 가능한 타입들
Union 타입
교차 타입이 타입 A와 타입 B 모두 만족이라는 것이라면 유니온 타입은 OR연산자(||)처럼 타입 A거나 타입 B이다의 의미를 가진다.
type Fruit = 'apple' | 'lemon';
type Vegetable = 'potato' | 'tomato';
type Food = Fruit | Vegetable;
// 'apple' | 'lemon' | 'potato' | 'tomato'
// Union Type은 type alias를 통해서만 정의 가능하다.
Tuple 타입
튜플(Tuple)은 배열 타입을 특수하게 사용할 수 있는 타입이다. 배치 순서, 타입 모두 튜플에 명시된 대로 정의해야 한다.
type Animal = [name: string, age: number];
const cat: Animal = ['', 1];
// Tuple Type은 type alias를 통해서만 정의 가능하다.
Mapped 타입
맵드(Mapped) 타입은 기존 정의된 타입을 새로운 타입으로 변환해 주는 문법을 의미한다. JavaScript의 map 메서드를 생각하면 된다.
type Vegetable = 'potato' | 'tomato';
//type VegetableOption = {
// potato: boolean;
// tomato: boolean;
//}
type VegetableOption = {
[Property in Vegetable]: boolean;
}
type VegetableOption = {
[Property in Vegetable]: boolean;
}
const option: VegetableOption = {
potato: true,
tomato: false,
};
// 'potato' | 'tomato'
type VegetableAlias = keyof VegetableOption;
Unknown 타입
알려지지 않은 타입이 있다면 typeof를 사용하여 타입확인이 가능하다.
const potato = {name: 'potato', weight: 1};
// type Vegatable = {
// name: string;
// weight: number;
// }
type Vegetable = typeof potato;
참고자료
https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html
https://yceffort.kr/2021/03/typescript-interface-vs-type
https://devocean.sk.com/blog/techBoardDetail.do?ID=165230&boardType=techBlog