그동안 4개의 포스트를 거치면서 타입 스크립트의 타입들에 대해서 알아봤습니다. 그런데 상관없어 보이는 개념처럼 보이는 각각의 타입들은 서로 긴밀한 상관 관계를 가지고 있습니다.
타입스크립트의 타입들은 위의 계층도 처럼 부모-자식 관계를 형성하고 있습니다. 다른 타입을 트리 아래에 갖는 타입을 부모 타입, 해당 부모 타입에 하위 트리로 존재하는 타입은 자식 타입이라고 부릅니다.
예를 들어
number enums
타입은number
타입이라고 구분해도 됩니다. 하지만 그 반대인number
타입을number enums
타입이라고 무작정 구분할 수는 없습니다.
그리고 자식 타입의 값을 부모 타입의 값으로 취급하는 행위를 업 캐스팅
, 부모 타입의 값을 자식 타입의 값으로 취급하는 행위를 다운 캐스팅
이라고 합니다. 계층도에 따라서 업 캐스팅
은 무조건 가능하지만, 다운 캐스팅
은 특정한 조건이 아니면 불가능합니다.
unknown
은 최상위 타입으로써 타입스크립트의 모든 타입을 받아들일 수 있습니다. 다시말해 타입스크립트의 모든 타입은 unknown
타입의 범주에 속한다라고 할 수 있습니다.
하지만 unknown
으로 정의된 타입의 값은 자식 타입의 변수, 함수에는 할당될 수 없습니다.
타입 계층에서 최하위 요소를 담당하는 타입은 never
입니다. never
타입으로 정의된 변수에는 어떠한 값을 포함할 수 없습니다.
아까 다운 캐스팅
을 이야기하면서 특정한 조건이 필요하다 했는데, 다운 캐스팅
이 가능한 상황이 바로 any
타입을 사용하는 경우입니다.
any
타입은 계층에서 최상위 unknown
바로 아래를 차지합니다. 즉, unknown
을 제외한 모든 타입의 부모 타입이 됩니다. 하지만 any
는 모든 타입의 자식 타입도 될 수 있습니다.
즉 any
타입은 모든 타입에 대해 부모 타입이면서 동시에 자식 타입이므로 any
타입에선 업 캐스팅
과 다운 캐스팅
이 모두 이루어질 수 있습니다.
let anyVariable: any;
//계층에서 부모 타입인 any를 자식 타입에 할당시킬 수 있는 다운 캐스팅
let num: number = anyVariable; //다운 캐스팅
let str: string = anyVariable; //다운 캐스팅
anyVariable = num; //업 캐스팅
anyVariable = str; //업 캐스팅
타입 호환성
은 두 개의 타입을 놓고서 A 타입을 B 타입으로 사용해도 괜찮을지를 확인하는 것입니다. 이때 A 타입이 B 타입으로 사용할 수 있다면 호환이 되는것이고 그렇지 않다면 호환되지 않는 다는 의미를 갖습니다.
즉 호환성은 타입 계층 구조에 따라서 한 타입을 다른 타입으로 사용할 수 있는지에 대해 판단하는 것 입니다. 그러므로 타입 계층 구조를 파악해 둔다면 타입스크립트를 사용하면서 호환성 문제에 부딛히는 문제가 줄어들겠죠?
지금까지는 변수만 가지고 호환성 이야기를 했습니다. 그렇다면 객체간의 호환성은 어떻게될까요?
객체들은 다른 객체들과 부모 자식관계를 갖습니다.
type Fruit = {
name: string;
taste: string;
};
type Peach = {
name: string;
taste: string;
color: string;
};
let fruit: Fruit = {
name: '포도',
taste: '새콤달콤',
};
let peach: Peach = {
name: '천도복숭아'
taste: '새콤달콤',
color: '담홍색',
};
위와같은 객체 둘이 있다고 가정합니다. 이때 다음과 같은 코드의 결과는 다음과 같습니다.
fruit = peach; //가능!!!
peach = fruit; //Error!!!
위 코드에서 Fruit 타입은 Peach 타입의 부모 타입이 될 수 있지만 그 반대는 불가능하다고 합니다. 하지만 코드를 봤을때는 프로퍼티가 하나 더 많은 Peach 타입이 Fruit 타입의 부모가 될 것 처럼 생기지 않았나요?
그 이유는 타입스크립트의 동작에 숨겨져있습니다. 타입스크립트는 구조적 타입 시스템
을 채택하고 있기 때문에 이러한 동작을 보이게 됩니다.
구조적 타입 시스템
이란건 뭘까요?
타입스크립트 코드를 컴파일하는 과정에서 Fruit 타입을 Peach 타입에 할당시킬 수 있는지를 검사합니다. 이 과정에서 컴파일러는 두 타입의 명시적 선언이 아닌 프로퍼티를 비교하게 됩니다.
비교 결과 한 타입이 다른 타입의 속성을 만족시킬 수 있다면 할당이 가능(부모-자식 관계가 가능)하다고 타입 추론을 하게 됩니다.
그래서 위 예제에서 fruit는 peach를 만족시킬 두 프로퍼티(name, taste
)를 가지고 있기에 fruit가 peach의 부모 타입이 된 것입니다. 반대로, fruit는 peach를 만족시키는 color
프로퍼티가 존재하지 않기에 peach가 fruit의 부모 타입이 될 수가 없는 것이죠.
좀 더 풀어서 이야기하자면, Fruit 타입은
name
과taste
프로퍼티를 가지고 있는 객체들을 자식으로 갖는 타입이고, Peach 타입은name
과taste
,color
프로퍼티를 가지고 있는 객체들을 자식으로만 가질 수 있다라고 표현할 수 있습니다.이 결과에 따르면 Peach 타입의 자식으로 어떤 타입이 온다면, 그 타입은 역시 Fruit의 자식 타입이 될 수 있다는 것도 의미합니다.
그러면 Fruit 타입의 자식으로 Peach 타입이 올 수 있으니까 다음과 같은 코드도 성립할까요?
type Fruit = {
name: string;
taste: string;
};
type Peach = {
name: string;
taste: string;
color: string;
};
let dounutPeach: Fruit = {
name: '납작 복숭아',
taste: '달콤',
color: '담홍색',
};
질문에 대한 답은 X입니다.
분명 위에서 Fruit 타입이 Peach 타입의 부모라고 했는데, Peach 타입의 프로퍼티를 갖는 객체를 왜 Fruit 타입으로 할당할 수 없을까요?
그 이유는 타입스크립트는 초과 프로퍼티 검사(excess property checks)
를 수행하기 때문입니다. 초과 프로퍼티 검사
는 타입에 정의된 프로퍼티 외에 다른 프로퍼티를 갖는 객체를 할당할 수 없게 만드는 기능입니다.
위 코드에서 Fruit에 정의된 타입이 아닌 프로퍼티인 color
가 포함되었기 때문에 해당 코드에서는 오류를 발생시키게 되는 것 입니다.