js는 본질적으로 덕 타이핑(duck typing) 기반이다.
덕 타이핑이란, 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식.
덕 테스트(duck test)에서 유래되었는데, 다음과 같은 명제로 정의 된다.
"만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리는 낸다면 나는 그 새를 오리라고 부를 것이다."
그렇기 때문에 만약 어떤 함수의 매개변수 값이 모두 제대로 주어진다면, 그 값이 어떻게 만들어졌는지 신경 쓰지 않고 사용한다.
ts는 이런 동작, 즉 매개변수 값이 요구사항을 만족한다면, 타입이 무엇인지 신경 쓰지 않는 동작을 그대로 모델링 한다.
테스트를 작성할 때는 구조적 타이핑이 유리하다.
라이브러리 간의 의존성을 완벽히 분리할 수 있다.
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return v.x + v.y;
}
interface NamedVector {
name: string;
x: number;
y: number;
}
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v) // 7
위 코드는 Vector2D와 NamedVector의 관계를 아래와 같이 따로 선언하지 않았지만
interface Vector2D {
x: number;
y: number;
}
interface NamedVector extends Vector2D{
name: string;
}
ts의 타입 시스템은 js의 런타임 동작을 모델링하고, NamedVector의 구조가 Vector2D의 구조와 호환되기 때문에 calculateLength 함수를 오류없이 호출 가능 했는데, 이것을 '구조적 타이핑(structural typing)'이라는 용어로 사용합니다.
(이런 경우 따로 오류로 처리하기 위한 설정이 존재하긴 함)
하지만 구조적 타이핑으로 인해서 가끔 당황스러운 결과가 발생하는 경우가 있습니다.
예를 들어서
|
백터의 길이를 1로 만들어 리턴하는 함수A가 있습니다.
길이를 연산하는 함수B는 함수A 내부에 있습니다.
interfaceL과 interfaceQ가 있습니다.
interfaceL과 interfaceQ는 서로 호환됩니다.
|
함수B는 interfaceL을 기반으로 연산을 하는데, 버그로 인해 interfaceQ로 연산되었습니다.
그 결과로 백터의 길이는 1이 아닌 1.~~를 리턴 했습니다.
이는 함수를 작성할 때, 호출에 사용되는 매개변수의 속성들이 매개변수의 타입에 선언된 속성만을 가질 거라 생각하기 쉽기 때문입니다.
이러한 타입은 '봉인된(sealed)' 또는 '정확한(precise)' 타입이라고 불리며, ts의 타입 시스템에서는 표현할 수 없습니다.
ts의 타입은 좋든 싫든 '열려(open)'있습니다.
열려 있다는 것은, 타입의 확장에 열려 있다는 의미입니다.
즉, 타입에 선언된 속성 외에 임의의 속성을 추가하더라도 오류가 발생하지 않는다는 것입니다.