자바스크립트는 덕 타이핑(duck typing) 기반이다. 덕 타이핑이란 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 그 타입에 속하는 것을 간주하는 방식이다. 어떤 함수와 매개변수의 값이 제대로 주어진다면, 어떻게 해서 이 값이 만들어졌는지는 생각하지 않고 사용한다. 타입스크립트 역시 마찬가지로 자바스크립트의 덕 타이핑을 모델링하기 위해 매개변수의 값이 요구사항을 만족한다면 타입이 무엇인지는 신경쓰지 않는다.
interface Vector2D {
x: number
y: number
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y)
}
interface NamedVector {
name: string
x: number
y: number
}
const v: NamedVector = { x: 3, y: 4, name: 'Zee' }
calculateLength(v) // 정상!!!! 답은 5
calculateLength의 인자로 NamedVector를 넣어도 제대로 동작한다. NamedVector를 위한 calculateLength를 따로 구현할 필요도 없다. Vector2D와 NamedVector의 관계를 전혀 선언한 적이 없음에도 잘 동작하는 이유는, NamedVector는 number 타입의 x와 y 속성이 있으므로 Vector2D와 호환되어 있기 때문에 그렇다. 이를 **구조적 타이핑(structural typing)**이라 한다.
구조적 타이핑이란 해당 객체의 타입이 어떤 타입과 구조적으로 맞다면 그 객체의 타입을 해당 타입으로 취급하는 타입 체킹 방식이다.
interface Vector3D {
x: number
y: number
z: number
}
function normalize(v: Vector3D) { // 벡터의 길이를 1로 만드는 정규화 함수
const length = calculateLength(v) // 출력값은 5. 실제로는 약 7.07임.
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
}
}
console.log(normalize({ x: 3, y: 4, z: 5 })) // { x: 0.6, y: 0.8, z: 1 }
// 총 길이가 1이 되지 않음. 1.41 정도?
calculateLength는 2D 벡터를 기반으로 계산되어야 함에도 3D 벡터를 인자로 받았을 때 이를 캐치하지 못했다. z를 제외하고 계산해버렸기 때문에 이런 문제가 발생했다.
왜 타입 체커가 이 오류를 알 수 없었을까? 구조적 타이핑의 관점에서 보았을 때 x와 y가 있었기 때문에 Vector2D와 호환되었기 때문에 그렇다.
함수의 매개변수들의 타입이 무조건 이미 선언되어 있는 타입만 가질 것이라고 생각하기 쉽지만, 이런 정확한(봉인된) 타입은 타입스크립트에서는 표현할 수 없다(런타임에서는 타입은 사라지기 때문에). 타입은 늘 열려 있다(타입의 확장이 열려 있다).
이런 구조적 타이핑 때문에 헷갈리는 상황들이 많이 등장한다.
function calculateLengthL1(v: Vector3D) {
let length = 0
for (const axis of Object.keys(v)) {
const coord = v[axis]
// Element implicitly has an 'any' type because
// expression of type 'string' can't be used to index type 'Vector3D'.
// No index signature with a parameter of type 'string' was found on type 'Vector3D'.
length += Math.abs(coord)
}
return length
}
이게 무슨 에러일까. axis는 v의 키이므로 “x”, “y”, “z” 중 하나일 것이다. 그리고 Vector3D의 선언을 보았을 때 v[axis]는 모두 number이므로 당연히 coord도 number일 것이라고 생각할 수 있다.
하지만 다음을 보자.
const vec3D = { x: 3, y: 4, z: 1, address: '123 Anyang'}
calculateLengthL1(vec3D)
사실상 위의 코드는 문제가 없다. 그 이유는 우리가 생각했던 것과 다르게 vec3D의 타입은 봉인되어 있지 않기 때문이다. 그러므로 v[axis]가 어떤 속성을 지닐지는 알 수 없으므로 타입스크립트로써는 제대로 타입 오류를 찾아낸 것이 맞다.
이런 경우에는 루프를 도는 것보다는 모든 속성을 다 더하는 것이 낫다.
function calculateLengthL1(v: Vector3D) {
return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z)
}
클래스에서도 구조적 타이핑은 유효하다. 구조적 타이핑으로 인해 클래스의 인스턴스가 예상과 다를 수 있다.