[이펙티브 타입스크립트] #4 구조적 타이핑에 익숙해지기
자바스크립트는 덕 타이핑(duck typing) 기반이다.
💡 덕 타이핑이란?
객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식이다. 이는 덕 테스트(The Duck Test)에서 유래되었는데, “만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.”
JS 에서 어떤 함수의 매개변수 구성 요소가 요구사항을 만족한다면, 타입이 무엇인지 신경 쓰지 않는 동작을 그대로 모델링합니다. 하지만 Type checker는 타입에 대한 이해도가 사람과 다른 면이 있어 예상치 못한 결과가 생기기도 함
⇒ 구조적 타이핑을 제대로 이해해야만 오류를 제대로 파악할 수 있고, 견고한 코드를 짤 수 있음
interface Vector2D {
x: number;
y: number;
}
function calcLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
// name 속성이 추가된 타입
interface NamedVector {
x: number;
y: number;
name: string;
}
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calcLength(v); // Not error,
NamedVector는 number 타입인 x, y 속성이 있기 때문에 calcLength의 인자로 사용 가능
- Vector2D와 NamedVector의 관계를 선언하지 않았음
- NamedVector 타입의 인자를 위한 별도의 함수(ex: calcLengthForNaemdVector)를 선언하지 않아도 된다
똑똑한 타입스크립트?! ⇒ 구조적 타이핑(Duck Typing)
구조적 타이핑으로 인해 발생하는 문제들
case #1
interface Vector3D {
x: number;
y: number;
z: number;
}
function normalize(v: Vector3D) {
const length = calcLength(v); // calcLength는 Vector2D 타입의 인자를 받는 함수
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
};
}
normalize({ x: 3, y: 4, z: 5 }); // { x: 0.6, y: 0.8, z: 1 }
Vector3D를 기반으로 연산하는데, Vector2D로 연산되면서, z가 정규화에서 무시
그럼에도 문제가 없었던 이유는 Vector3D에 x, y 속성이 있기 때문에(구조적 타이핑) Type Checker가 문제로 인식하지 않았음 (tsConfig에 이런 경우를 오류로 처리하는 설정이 있음)
case #2
우리는 함수를 작성할 때, 호출에 사용되는 매개변수의 속성들이 매개변수의 타입에 선언된 속성만을 가질 거라 생각하기 쉽다. 이러한 타입을 봉인된(sealed) 또는 정확한(precise) 타입이라 부르며, typescript 타입 시스템에서는 표현할 수 없다. 좋든 싫든 타입의 확장에 열려(open) 있다.
function calcLength1(v: Vector3D) {
let length = 0;
for(const axis of Object.keys(v)) {
const coord = v[axis]; // 에러 발생, why??
length += Math.abs(coord);
}
return length;
}
아래의 사고 과정으로 인해, 위 에러를 납득하기는 어렵다.
- axis는 v의 key 중 하나 이므로 ‘x’ , ‘y’, ‘z’ 중 하나여야 한다.
- x, y, z 모두 number type이므로 coord의 타입은 number가 되어야 할 것으로 예상된다.
⇒ 하지만 type checker가 오류를 정확하게 찾아낸 것이 맞다. 왜냐하면 vector3D를 sealed 혹은 precise 타입으로 가정했기 때문이다.
const vec3D = { x: 3, y: 4, z: 1, address: '123 Broadway' };
calcLength1(vec3D); // NaN
v는 Vector3D에서 정의한 속성 이외에 어떤 속성이든지 가질 수 있다. (타입의 확장에 열려(open) 있기 때문)
그러므로 v[axis]가 number라고 확정할 수 없다.
function calcLength1(v: Vector3D) {
return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}
이런 경우 loop보다는 명시적으로 모든 속성을 각각 더하는 구현이 더 낫다.
case #3: 클래스와 관련된 할당문
class C {
foo: string
constructor(foo: string) {
this.foo = foo;
}
}
const c = new C('instant of C');
const d: C = { foo: 'object literal' };
Java, C# 개발자에게 다음과 같은 코드를 보여주면 매우 기겁하면서 도망을 가려고 할 것이다.
변수 d에 타입 C를 할당될 수 있는 이유
- d는 string 타입의 foo라는 속성을 지닌 객체 가진다.
- 생성자(Object.prototype)를 가진다 (객체 리터럴 방식 내부 동작)
구조적 타이핑과 테스트
테스트를 작성할 때는 구조적 타이핑이 유리하다.
interface Author {
first: string;
last: string;
}
// AS-IS
// getAuthors함수를 테스트하기 위해서는 모킹(mocking)한 PostgresDB interface를 생성해야 함
function getAuthors(database: PostgresDB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
return authorRows.map(row => ({ first: row[0], last: row[1] });
}
// TO-BE
interface DB {
runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
return authorRows.map(row => ({ first: row[0], last: row[1] });
}
getAuthors함수를 테스트하기 위해서는 모킹(mocking)한 PostgresDB interface를 생성해야 한다. 이것보다는 구조적 타이핑을 활용하여 인터페이스를 정의하는 것이 더 나은 방법이다. DB 인터페이스에 runQuery메서드가 있기 때문에 실제 환경에서도 인터페이스를 사용 할 수 있다.
test('getAuthors', () => {
const authors = getAuthors({
runQuery(sql: string) => {
return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
}
});
expect(authors).toEqual([
{ first: 'Toni', last: 'Morrison' },
{ first: 'Maya', last: 'Angelou' }
]);
});
테스트를 작성할 때, 더 간단한 개체를 매개변수로 사용할 수도 있다.
- 타입스크립트는 테스트 DB가 해당 인터페이스를 충족하는지 확인한다.
- 테스트코드에는 실제 환경의 데이터베이스에 대한 정보가 불필요
⇒ DB 인터페이스로 추상화 함으로써 로직과 테스트를 특정한 구현(PostgresDB)으로부터 분리하였다.
(추가) 테스트 이외에 구조적 타이핑 장점 → 라이브러리 간의 의존성을 완벽히 분리할 수 있다
요약
- JS는 덕 타이핑 기반, 타입스크립는 이를 모델링하기 위해 구조적 타이핑을 사용한다. 어떤 인터페이스에 할당 가능한 값이라면 타입 선언에 명시적으로 나열된 속성들을 가지고 있다.
- 타입은 ‘봉인(sealed)'되어 있지 않다. (확장 가능하다)
- 클래스 역시 구조적 타이핑 규칙을 따른다. 클래스의 인스턴스가 예상과 다를 수 있다
- 구조적 타이핑을 사용하면 유닛 테스팅을 손쉽게 할 수 있다.
'프로그래밍 > Typescript' 카테고리의 다른 글
[이펙티브 타입스크립트] 객체 래퍼 타입 피하기 (0) | 2022.06.10 |
---|---|
[이펙티브 타입스크립트] 타입 공간과 값 공간의 심벌 구분하기 (0) | 2022.06.05 |
[TypeScript] e.target.target 타입 에러가 나는 이유 ts(2339) (0) | 2022.06.04 |
댓글
이 글 공유하기
다른 글
-
[이펙티브 타입스크립트] 객체 래퍼 타입 피하기
[이펙티브 타입스크립트] 객체 래퍼 타입 피하기
2022.06.10 -
[이펙티브 타입스크립트] 타입 공간과 값 공간의 심벌 구분하기
[이펙티브 타입스크립트] 타입 공간과 값 공간의 심벌 구분하기
2022.06.05 -
[TypeScript] e.target.target 타입 에러가 나는 이유 ts(2339)
[TypeScript] e.target.target 타입 에러가 나는 이유 ts(2339)
2022.06.04