들어가며
타입스크립트를 사용하곤 있지만 타입 시스템에 대해서는 잘 모르고 있다는 생각이 들어 관련 공부를 시작했습니다. “타입스크립트는 구조적 타이핑을 사용한다”라는 내용의 글은 많지만 **“왜 타입스크립트는 구조적 타이핑을 사용할까?”**라는 글은 찾아볼 수 없었습니다. (어쩌면 당연한 내용이라 없는 걸까요?)
“타입스크립트가 명목적 타이핑을 사용하면 안되는 이유는 무엇일까?” “구조적 타이핑의 장점은 뭐지?” 등등의 궁금증이 생기기 시작하여 조사를 해보았습니다.
관련 레퍼런스를 많이 찾아봤지만 깔끔하게 정리된 내용을 찾지 못했습니다. 따라서 주관적인 생각을 많이 들어간 글임을 먼저 밝힙니다.
구조적 타이핑(Structural typing)과 명목적 타이핑(Nominal typing)
타입스크립트의 타입 시스템은 구조적 타이핑
을 사용합니다. 이는 Java, C# 언어가 사용하는 명목적 타이핑
시스템과는 다른 특징을 보입니다.
구조적 타이핑은 _“멤버가 호환되면 타입이 호환된다”_라는 특징을 지닙니다. 따라서 아래 코드는 정상적입니다.
class Pet {
name: string;
breed: string;
}
class Dog {
name: string;
breed: string;
age: number;
}
// ✅ 구조적 타이핑 - OK
// Dog은 Pet과 호환가능한 멤버 "name", "breed"를 가지고 있기 때문에 호환가능합니다.
let pet: Pet = new Dog();
이에 반해 명목적 타이핑(nominal typing) 시스템은 오로지 타입 이름으로 타입 호환여부를 판단합니다. 완전히 동일한 멤버를 가지고 있더라도 타입 이름이 다르면 다른 타입으로 판단합니다.
class Pet {
name: string;
breed: string;
}
class Dog {
name: string;
breed: string;
}
// ❌ 명목적 타이핑 - Not OK
// Dog은 Pet과 서로 다른 이름의 타입을 가지고 있기 때문에 호환불가합니다.
let pet: Pet = new Dog();
코드만 본다면 명목적 타이핑이 구조적 타이핑보다 훨씬 안전한 타입 시스템으로 보입니다. 구조적 타이핑의 “유연한 타입”은 개발자의 실수를 완전히 잡아내지 못하는 타입 시스템처럼 보입니다.
우리가 타입스크립트를 사용하는 이유가 자바스크립트에 타입을 부여하여 안전한 코드를 작성하는 것이라면, 왜 타입스크립트는 훨씬 더 안전한 명목적 타이핑 대신 구조적 타이핑을 사용하는 것일까요?
타입 안정성 측면에서만 본다면 구조적 타이핑은 나쁜것 같습니다. 하지만 타입 시스템이 항상 엄격하고 안정성만을 추구할 필요는 없어보입니다.
타입스크립트가 구조적 타이핑을 사용하는 이유
타입스크립트가 구조적 타이핑을 사용하는 이유를 이해하기 위해서는 타입스크립트 언어가 가진 특성을 알아야합니다. 타입스크립트는 기존의 존재하는 자바스크립트를 대체하기 위해 만들어진 슈퍼셋(상위 집합) 언어입니다.
모든 자바스크립트 코드는 타입스크립트 코드입니다.
타입스크립트는 완전히 새로운 언어가 아닙니다. 자바스크립트 생태계를 그대로 이어받으려고 하는 슈퍼셋 언어입니다. 덕분에 우리는 자바스크립트 라이브러리를 오로지 타입 정보만 추가하여 타입스크립트에서 사용할 수 있습니다. (@types 라이브러리)
그렇다면 기존에 존재하는 아래 자바스크립트 코드를 타입스크립트 코드로 바꾸고 싶다면 어떻게 해야할까요?
function addTodo(todo){ }
addTodo({
done: true,
title: '오늘 할 일'
})
구조적 타이핑을 사용한다면 오로지 타입 정보
를 가지는 코드만 작성하면 됩니다. 이러한 타입 정보는 컴파일 중에 type erase
되어 제거됩니다. 결과적으로 런타임에는 아무런 영향을 주지 않는 것입니다.
interface Todo {
done: boolean;
title: string;
}
// 런타임에 영향을 주지 않는 타입 정보 코드만 추가하면 됩니다.
function addTodo(todo: Todo){ }
addTodo({
done: true,
title: '오늘 할 일'
})
명목적 타이핑에서는 이 코드는 컴파일 불가능합니다. 왜냐하면 익명 객체 리터럴은 Todo 타입이 아니기 때문입니다.
interface Todo {
done: boolean;
title: string;
}
function addTodo(todo: Todo){ }
// ❌ Error 명목적 타이핑에서는 익명 객체 리터럴은 Todo 타입과 호환불가능합니다!
addTodo({
done: true,
title: '오늘 할 일'
})
자바스크립트에서 흔하게 사용하는 코드 작성 방법이 명목적 타이핑 타입 안정성과는 맞지않게 되었습니다.
동작을 위해 단순히 타입 정보만을 추가하는 것이 아닌, 런타임에도 영향을 주는 추가적인 리팩토링이 필요합니다.
interface Todo {
done: boolean;
title: string;
}
function addTodo(todo: Todo){ }
// 런타임에도 영향을 주는 코드를 작성해야합니다.
const todayTodo: Todo = {
done: true,
title: '오늘 할 일'
};
addTodo(todayTodo)
자바스크립트 코드를 타입스크립트에서 사용하기 위해 타입코드 말고도 런타임에 영향을 주는 추가적인 코드가 필요해졌습니다.
모든 자바스크립트 코드가 타입스크립트 코드가 아니게 되었습니다.
기존 자바스크립트를 다시 작성해야하는 명목적 타이핑보다는 구조적 타이핑이 타입스크립트과 맞아 보입니다.
사실 이러한 타입스크립트의 구조적 타이핑은 자바스크립트가 가지고 있는 덕 타이핑
특징과 같습니다. 차이점이라면 타입스크립트는 정적
으로, 자바스크립트는 동적(런타임)
에 타입을 결정한다는 점입니다. (구조적 타이핑을 정적 덕 타이핑이라고도 합니다)
아래 자바스크립트 코드는 자연스럽습니다. 런타임에 동일한 멤버를 가지고 있다면 정상적으로 동작합니다.
class Duck {
sayHello(){
console.log("나는 오리");
}
}
class Dog {
sayHello(){
console.log("나는 강아지");
}
}
function sayHello(dog){
dog.sayHello();
}
// ✅ OK - 런타임에 정상적으로 동작합니다.
// 동일한 멤버를 가지고 있다면 Dog인지, Duck인지 상관없습니다.
sayHello(new Duck());
sayHello(new Dog());
타입스크립트는 이러한 자바스크립트의 덕 타이핑
특성을 정적인 시점에 그대로 사용합니다.
다음은 타입스크립트가 밝힌 타입스크립트 디자인 원칙
중 일부입니다.
- 모든 자바스크립트 코드의 런타임 동작을 유지합니다
- 일관되고 완전히 지울 수 있는 타입 시스템을 사용합니다.
- “확실하게 올바른” 타입 시스템 대신, 정확성과 생산성 사이의 균형을 유지합니다.
- 추가적인 런타임 타입 정보를 추가하는 대신, 런타임 정보가 필요없는 프로그래밍 패턴을 권장합니다.
타입스크립트가 구조적 타이핑을 사용하는 이유는 자바스크립트가 가지는 언어적인 특성 + 하위 호환성을 지키기 위한 슈퍼셋 언어라는 디자인 목표 때문이라는 것을 알 수 있습니다.
타입스크립트에서 명목적 타이핑 흉내내기
타입스크립트가 구조적 타이핑을 사용하는 이유에 대해 알아보았습니다. 구조적 타이핑은 명목적 타이핑에 비해 타입 안정성은 떨어지지만 유연합니다.
**하지만 코드를 작성하다보면 엄격한 타입 안정성을 필요로 할 때가 있습니다. **
interface Point2D {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
function handlePoint2D(point: Point2D) {}
function getPoint3D(): Point3D {
return { x: 0, y: 0, z: 0 };
}
// ✅ OK 구조적 타이핑에서는 허용됩니다.
// 하지만 명백한 개발지의 실수를 타입스크립트는 허용합니다.
handlePoint2D(getPoint3D());
엄격하게 타입 안정성을 지키고 싶을때 사용하는 기법이 있습니다. 바로 타입 브랜딩
입니다. 서로 다른 타입이 호환되지 않도록 유니크한 브랜드 멤버를 추가합니다.
interface Point2D {
// 브랜드 멤버
__brand: "Point2D";
x: number;
y: number;
}
interface Point3D {
// 브랜드 멤버
__brand: "Point3D";
x: number;
y: number;
z: number;
}
function handlePoint2D(point: Point2D) {}
function getPoint3D(): Point3D {
return <Point3D>{ x: 0, y: 0, z: 0 };
}
// ❌ __brand 속성의 형식이 호환되지 않습니다. 타입 안정성이 높아졌습니다.
handlePoint2D(getPoint3D());
하지만 타입 브랜딩 기법은 어디까지나 명목적 타이핑을 흉내내는 것에 불과합니다. 타입스크립트는 언어 차원에서 아직까지는 명목적 타이핑을 지원하고 있지 않고 있습니다. 계속해서 명목적 타이핑을 도입하고자 하는 움직임이 있습니다. 계속해서 명목적 타이핑에 대한 논의가 이루어지고 있습니다. 타입스크립트는 이미 구조적 타이핑을 사용하고 있는데 어떻게 명목적 타이핑을 사용할 수 있을까? 라는 궁금증이 생길 수 있습니다.
사실 타입 시스템은 혼합하여 사용될 수 있습니다. 실제로 페이스북이 만든 정적 타입 검사기 Flow
는 함수나 객체에는 구조적 타이핑을 사용하고 Class에는 명목적 타이핑을 사용하고 있어 두 타이핑을 혼합해서 사용하고 있습니다.
// @flow
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }
// Flow는 Class에 명목적 타이핑을 사용하고 함수나 객체에는 구조적 타이핑을 혼합해서 사용하고 있습니다.
let test: Foo = new Bar(); // Error!
정리
간단하게 정리하면 다음과 같습니다.
- 구조적 타이핑은 “적당히 엄격하고 적당히 느슨한” 타입 시스템입니다. “확실하게 올바른” 타입 검사를 목표로하는 시스템이 아닙니다.
- 자바스크립트 언어가 가지고 있는 특성을 지키기 위해서는 명목적 타입 시스템보다는 구조적 타입 시스템을 이용하여 표현하는 것이 훨씬 더 자연스럽습니다.