23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
프론트엔드 개발자들의 언어인 타입스크립트에서는 type과 interface 라는 두 가지 키워드로 객체의 타입을 정의할 수 있는데요,
이번 블로그에서 두 키워드의 사용 방법이 다른 것 외에도 기능적으로 어떤 차이가 있는지를 소개 합니다.
타입스크립트는 한 객체가 다른 객체를 상속할 수 있게 해주는 interface 라는 일급 클래스 원시 타입(first-class primitive)을 제공한다.
interface WithId {
id: string
}
interface User extends WithId {
name: string
}
const user: User = {
id: '0623',
name: 'chanmin',
wrongProperty: 123, // Error!
}인터페이스를 대체하기 위한 type 키워드 역시 존재한다.
type 키워드는 객체 타입뿐만 아닌 모든 유형의 타입을 정의할 수 있다.
type StringOrNumber = string | number
const func = (arg: StringOrNumber) => {}
func('hello')
func(123)
func(true) // Error!여기서 논란이 발생하는데, 객체 타입을 정의할 때는 과연 type 을 사용해야 할까 interface 를 사용해야 할까?
서로 상속 관계에 있는 객체를 정의할 때는 interface 를 활용한다.
위의 코드 중 interface 를 type 으로 고쳐 보자.
type WithId {
id: string;
}
type User & WithId {
name: string;
}
const user: User = {
id: "0623",
name: "chanmin",
// Error
// `Type '{ id: string; name: string; wrongProperty: number; }' is not assignable to type 'User'.
// Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
wrongProperty: 123,
};타입스크립트는 인터페이스 이름을 기반으로 인터페이스의 정보를 내부 레지스트리에 캐싱할 수 있어, extends를 사용할 때는 캐싱의 장점을 활용할 수 있다.
그러나 유니온(&) 연산자를 활용하는 경우에는 이름을 기반으로 캐싱할 수 없고, 항상 연산을 수행하기 때문에 성능상 불리하다.
레퍼런스 : https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections
이런 장점에도 불구하고 인터페이스를 기본으로 사용하는 것은 추천되지 않는다.
인터페이스는 스코프 내에 정의된 같은 이름의 서로 다른 인터페이스를 하나로 병합한다.
잠깐 변수를 할당하는 과정을 떠올려 보자.
let user = {
name: 'chanmin',
}
user = {
age: 26,
}
// user 객체는 { age: 26 } 이 된다.변수에 할당이 중복으로 이루어진다면 일반적으로는 나중에 선언된 할당이 이루어져야 한다.
(const 로 정의되었다면 재할당을 방지할 수도 있다.)
그러나 인터페이스를 중복으로 정의할 때는 이와 다르게 동작한다.
interface User {
name: string
}
interface User {
id: string
}
// 1. 스코프 내에 User 인터페이스가 중복으로 선언되었음에도, 이를 오류로 판단하지 않는다.
// (* 마치 var 처럼 동작한다.)
const user: User = {
// Error
// "Property 'name' is missing in type '{ id: string; }' but required in type 'User'."
id: '123',
}먼저 한 스코프 안에 User 라는 인터페이스를 중복으로 정의했음에도 var 키워드처럼 중복 할당을 허용하고 있다.
그런데 더 문제가 되는 것은, 나중에 할당한 인터페이스 정의가 User 인터페이스를 덮어쓰는 것이 아니라 같은 이름의 서로 다른 인터페이스들의 속성을 모두 병합한다는 것이다.
반면 interface 대신 type 을 활용하면 이런 문제가 발생하지 않는다.
// Error: Duplicate identifier 'User'.
type User = {
name: string;
}
// Error: Duplicate identifier 'User'.
type User = {
id: string;
}
const user: User = {
id: string;
}객체를 타입으로 정의하면 동일 스코프에서 동일한 이름의 타입에 중복으로 할당하는 것을 방지해, const 키워드를 사용한 것처럼 동작한다.
TIP. 인터페이스 병합 문제를 차단하기 위해서는
no-redeclareESLint 룰을 적용할 수 있다.
타입은 암묵적인 인덱스 시그니처를 갖는 반면 인터페이스는 그렇지 않다.
즉 타입은 인덱스 시그니처를 갖는 타입에 할당할 수 있지만, 인터페이스는 할당할 수 없다.
interface KnownAttributes {
x: number
y: number
}
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
}
type RecordType = Record<string, number>
// Error
// `Type 'KnownAttributes' is not assignable to type 'RecordType'.
// `Index signature for type 'string' is missing in type 'KnownAttributes'.`
// KnownAttributes 인터페이스에 <string, number> 외에 다른 타입이 확장될 수 있을 것으로 추론한다.
const oi: RecordType = knownAttributes
Type 'KnownAttributes' is not assignable to type 'RecordType'.Index signature for type 'string' is missing in type 'KnownAttributes'.`
인터페이스에서 이를 지원하지 않는 까닭은 인터페이스는 추후 확장될 수 있는 가능성이 있기 때문이다. 인터페이스에 인덱스 시그니처를 활용하려면 명시적으로 정의해줘야 한다.
interface KnownAttributes {
x: number
y: number
[index: string]: unknown
}또는 단순히 type 으로 변환해 사용할 수 있다.
type KnownAttributes = {
x: number
y: number
}
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
}
type RecordType = Record<string, number>
const oi: RecordType = knownAttributesTS 팀에서는 interface를 기본으로, type을 필요할 때만 사용하도록 권장한다. 그러나 Declaration Merging 및 인덱스 시그니처는 기본적으로 interface를 사용하기 꺼려지도록 한다.
문서에서는 문제가 생기면 컴파일러가 알려줄 테니 개인 취향에 따라 자유롭게 사용하되, type 의 기능이 필요한 것이 아니라면 휴리스틱한 사용을 위해 interface 를 사용할 것을 권장한다.
암묵적인 인덱스 시그니처
재선언 불가 (const 키워드의 역할)
상속이 필요한 경우 캐싱을 활용할 수 있다.
interface 라는 이름의 특성 상, 보다 휴리스틱한 사용이 가능하다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.