타입스크립트에서 타입을 정의하는 방법은 type
, interface
두 가지가 있다.
대부분의 경우 타입 또는 인터페이스를 사용해도 된다. 하지만 타입과 인터페이스 사이에 존재하는 차이를 분명하게 알고,
같은 상황에서는 동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야한다.
interface IPerson {
name: string
}
type TPerson = { name: string }
const iperson: IPerson = { name: 'koo', age: 15 } // Type Error
const tperson: TPerson = { name: 'koo', age: 15 } // Type Error
type TDict = { [key: string]: string }
interface IDict {
[key: string]: string
}
type TFn = (x: number) => void
interface IFn {
(x: number) => void
}
type TFn = {
(x: number) => void
props: string
}
interface IFn {
(x: number) => void
props: string
}
type TFn<T> = {
props: T
}
interface IFn<T> {
props: T
}
인터페이스는 타입을 확장할 수 있고, 타입은 인터페이스를 확장할 수 있다.
type A = { name: string }
interface B { age: string }
interface C extends A { }
type D = B & A
인터페이스에는 union
이라는 개념이 없다. 따라서 인터페이스는 union type
을 확장할 수 없다.
type A = { name: string }
type B = { age: string }
type ABUnion = A | B
type ABIntersection = A & B
interface D extends ABIntersection { }
interface E extends ABUnion { } // Type Error
따라서 아래와 같은 타입은 인터페이스로 표현할 수 없다.
type NamedVariable = ({a: string} | {b: string}) & {name: string}
tuple
은 인터페이스로 구현할 수는 있지만 타입으로 구현하는 것이 좋다.
type TPair = [number, number]
interface IPair {
0: number
1: number
length: 2
}
아래의 코드를 보면 인터페이스가 중복으로 선언된 것 같지만 올바른 문법이고 아래처럼 속성을 확장하는 것을 선언병합(declaration merging)
이라고 한다.
interface A { name: string }
interface A { age: number }
const person: A = { name:'koo', age: 15 }
선언 병합은 주로 타입 선언 파일에서 사용되고 타입 선언 파일을 작성할 때는 선언 병합을 지원하기 위해서 반드시 인터페이스를 사용해야한다.
예를 들면 Array
인터페이스는 lib.es5.d.ts
에 정의되어있는데, tsconfig.json
의 lib
목록에 es2015
를 추가하면 타입스크립트는 lib.es2015.d.ts
에 선언된 인터페이스를 병합한다. 여기에는 ES2015에 추가된 find
같은 매서드가 포함된다. 결과적으로 각 선언이 병합되어 전체 매서드를 가지는 Array
타입을 가지게 된다.
하지만 프로젝트 내부적으로 사용되는 타입에 선언 병합을 한다면 잘못된 설계임을 기억하자.
- 인터페이스로 표현할 수 없는 유니온의 경우 타입으로 선언
- 타입과 인터페이스 둘다 표현가능하다면, 기존 코드와 일관되게 표현
- 어떤 외부 API에 대한 타입을 보강한다면 반드시 인터페이스를 사용