[이펙티브 타입스크립트] 아이템 37 - 공식 명칭에는 상표 붙이기

Minha Ahn·2023년 8월 22일
1
post-thumbnail

🕹️공식 명칭(nominal typing)을 사용하기

  • 공식 명칭 개념을 타입스크립트에서 흉내 내려면 ‘상표(brand)’를 붙이면 된다.
    • 거창한게 아니다. 그냥 타입 내부에 brand 속성을 추가하면 된다.
/** brand를 적용하지 않을 때 발생하는 문제 상황 */
interface Vercotr2D {
	x: number;
	y: number;
}

function calculateNorm(p: Vector2D) {
	return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm({x: 3, y: 4});       // 문제없는 코드
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);              // 구조적 타이핑 관점으로 문제없는 코드
/** brand를 적용한 상황 */
interface Vector2D {
	_brand: '2d';  // 상표를 사용해서 calculateNorm 함수가 Vector2D 타입만 받는 것을 보장.
	x: number;
	y: number;
}

// x와 y값을 받아 brand를 붙여 Vector2D 타입으로 만들어주는 함수
function vec2D(x: number, y: number): Vector2D {
	return {x, y, _brand: '2d'};
}

function calculateNorm(p: Vector2D) {
	return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm(vec2D(3, 4));
const vec3D = {x: 3, y: 4, z: 1};  // 여기에 _brand를 추가한다면 어쩔 수 없음.. 적어도 실수 방지는 할 수 있다.
calculateNorm(vec3D);

💥상표 기법의 효과

  • 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과
    • 타입 시스템 → 런타임 오버헤드 제거
    • 타입 시스템 → 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입도 상표화 가능
// 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입 상표화 가능
// 온전히 타입 시스템의 영역
type AbsolutePath = string & { _brand: 'abs' };  // 절대 경로 타입

function listAbsolutePath(path: AbsolutePath) {
	// ...
}

// 절대 경로, 상대 경로 판단 함수
// isAbsolutePath 함수의 리턴값이 true이면
// 함수를 호출한 범위 내에서 path의 타입은 AbsolutePath이다.
function isAbsolutePath(path: string): path is AbsolutePath {
	return path.startsWith('/');
}
// path에 절대 경로와 상대 경로 둘 다 가능하다면
// 타입 정제를 해주는 타입 가드를 사용해서 오류 방지!
function f(path: string) {
	if (isAbsolutePath(path)) {
		listAbsoultePath(path);     // 오류가 발생하지 않는다.
	}
	listAbsolutePath(path);       // 오류가 발생한다. path가 string이기 때문
}

🔍 타입 가드란? 특정 범위 안에서 런타임 타입 검사를 수행하는 표현식이며 컴파일러가 타입을 예측할 수 있도록 코드를 작성해서 버그가 발생하지 않도록 예방하는 방법이다. 하지만 여기에서는 단순이 타입을 방어한다는 느낌으로 작성된 것 같다.

🔍상표 기법이 쓰이는 또 다른 경우!

  • 상표 기법은 타입 시스템 내에서 표현할 수 없는 수많은 속성들을 모델링하는 데 사용된다.
    • 예시) 이진 검색 - 이진 검색은 이미 정렬이 되어있다는 가정이 깔려 있기 때문에 정렬되지 않는 목록을 이진 검색 하면 문제가 발생한다. 타입 시스템에서는 목록이 정렬되어 있다는 의도를 표현하기 어려우므로 다음과 같이 상표 기법을 사용할 수 있다.
type SortedList<T> = T[] & { _brand: 'sorted' };

// 정렬이 되어있는지 목록 전체를 루프 도는 방법 (효율적 X. 안정성 확 O)
function isSorted<T>(xs: T[]): xs is SortedList<T> {
	for(let i = 1; i < xs.length; i++) {
		if(xs[i] < xs[i-1]) {
			return false;
		}
	}
	return true;
}

// 이진 검색을 하는 함수
// 아래처럼 정렬되었다는 상표가 붙은 SortedList 타입 값을 호출하거나
// isSorted를 호출하여 정렬되었음을 증명하거나
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
	// ...
}
  • 숫자 단위를 문서화 하는 데 사용된다.
    • 숫자 단위 문서화는 코드에 여러 단위가 혼합된 많은 수의 숫자가 들어 있는 경우에 사용
    • 단, number 타입에 상표를 붙여도 산술 연산 후에는 상표가 없어지므로 실제로 사용하기에는 무리가 있다.
type Meters = number & ( _brand: 'meters' };
type Seconds = number & { _brand: 'seconds' };

const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;

const oneKm = meters(1000);   // 타입이 Meters
const oneMin = seconds(60);   // 타입이 Seconds
const tenKim = oneKm * 10;    // 타입이 number
const v = oneKm / oneMin;     // 타입이 number

💡결론

  • 타입스크립트는 값을 세밀하게 구분하지 못하는 경우가 있으므로 공식 명칭이 필요하다면 상표를 붙여보자.
  • 상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.
profile
프론트엔드를 공부하고 있는 학생입니다🐌

1개의 댓글

좋은 정보 감사합니다!

답글 달기