[Effective TypeScript] 타입 설계(2)

이예슬·2022년 12월 11일
0

Effective TypeScript

목록 보기
12/15

Item35. 데이터가 아닌, API와 명세를 보고 타입 만들기

외부에서 비롯된 데이터에 대한 타입을 작성할 때는 예시데이터가 아니라 명세를 참고해 타입을 작성하여야 한다. 명세를 참고해 타입을 생성하면 타입스크립트는 사용자가 실수를 줄일 수 있게 도와준다.

import { Feature } from 'geojson';

function calculateBoundingBox(f: Feature) : BoundingBox | null {
	let box: BoundingBox | null ;
	const helper = (coords: any[]) => {
	...
	} 
	const { geometry } = f; 
	if(geometry) {
		helper(geometry.coodinates);
		//❗️'Geometry' 형식에 coodinates 속성이 없습니다. 
	} 
	return box;
} 

위 예시는 데이터가 아닌 API와 명세를 보고 타입을 만들어야 하는 이유를 보여주고 있다. geometry 형식에 coordinate 속성이 있을 것이라고 생각하고 코드를 작성했지만 GeoJSON의 GeometryCollection에는 coordinate 속성이 존재하지 않는다. 명세를 기반으로 작성하지 않으면 이처럼 예상하지 못한 에러가 발생할 수 있다.

반면 명세를 기반으로 타입을 작성하면 현재까지 경험한 데이터 뿐 아니라 사용 가능한 모든 값에 대해 작동한다는 확신을 가질 수 있다. 또한 데이터에 드러나지 않는 예외적인 경우들이 문제가 될 수 있으므로 데이터보다는 명세로부터 코드를 생성하는 것이 좋다.

책에서는 특히 GraphQL과 같이 자체적으로 타입이 정의된 API에서 잘 동작한다고 한다.

(GraphQL은 아직 써본 적이 없어서 예시가 완전히 이해가지는 않아 해당 예시는 가져오지 않았다.)

Item36. 해당 분야의 용어로 타입 이름 짓기

컴퓨터 과학에서 어려운 일은 단 두 가지뿐이다. 캐시 무효화와 이름 짓기

엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드와 타입의 추상화 수준을 높여준다.

interface Animal {
	name: string;
	endangered: boolean;
	habitat: string;
} 

const leopard: Animal {
	name: 'Snow Leopard',
	endangered: false, 
	habitat: 'tundra',
} 

위 코드의 Animal 타입은 불분명하다. name이라는 일반적인 용어가 동물의 학명을 가르키는지 일반적인 명칭을 가르키는지 알 수 없으며 나머지 속성 또한 분명하지 못하다.

interface Animal {
	commonName: string;
	genus: string; 
	species: string; 
	status: ComsevationStatus; 
} 

type ConservationStatus = 'Ex' | 'EW' | 'CR' | 'EN' | 'VU' ...

const snowLeopard : Animal = {
	commonName : 'Snow Leopard', 
	genus: 'Panthera', 
	species: 'Uncia',
	status: 'VU', // 취약종(vulnerable) 
} 

위의 코드는 Animal 타입을 좀 더 명확하게 개선한 코드이다. 먼저 이름을 좀 더 구체적인 용어로 대체했으며 endagered는 동물 보호 등급에 대한 IUCN의 표준 분류 체계인 ConservationStatus 타입의 status로 변경했다. 처음의 타입보다 데이터를 훨씬 명확하게 표현하고 있다는 것을 느낄 수 있다.

이처럼 타입의 이름을 지을 때에는 자체적으로 용어를 만들어 내려고 하지 말고 핻아 분야에 이미 존재하는 용어를 사용해야 한다. 전문 용어를 사용하게 될 경우 정보를 찾기 위해 사람에 의존할 필요가 없으며 해당 용어에 대해 찾아보면 작성자의 의도를 파악할 수 있기 때문이다.

타입, 속성, 변수에 이름을 붙일 때 명심해야 할 규칙은 다음과 같다.

  • 동일한 의미를 나타낼 때는 같은 용어를 사용해야 한다.
  • data, info, thing, item, Object, entity 같은 모호하고 의미없는 이름은 피해야 한다.
  • 이름을 지을 때는 포함된 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지 고려해야 한다. 좋은 이름은 추상화의 수준을 높이고 의도치 않은 충돌의 위험성을 줄여준다.

Item37. 공식 명칭에는 상표 붙이기

타입스크립트는 구조적 타이핑을 사용하므로 값을 세밀하게 구분하지 못하는 경우가 있다. 때문에 값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 한다.

interface Vector2D {
	_brand: '2D';
	x: number;
	y: number
}

function vec2D(x: number, y: number): Vector2D {
	return {x, y, _brand: '2D' } 
} 
fucntion calculateNorm(p: Vector2D) {
	return Math.sqrt(p.x * p.x + p.y * p.y) 
} 

calculateNorm(vec2D(3, 4)); // 5
const vec3D = {x: 3, y: 4, z: 1}
caculateNorm(vec3D) //❗️'_brand' 속성이 ... 형식에 없습니다. 

위의 코드는 _brand 라는 상표를 사용해서 calculateNorm 함수가 Vector2D타입만 받는 것을 보장한다. 하지만 vec3D 값에 _brand: ‘2d’ 라고 추가하는 것 같은 악의적인 사용은 막을 수 없다. 다만 단순한 실수를 방지할 수 있다.

상표 기법은 타입 시스템에 의해 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다. 타입 시스템이기 때문에 런타임 오버헤드를 없앨 수 있고 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입도 상표화할 수 있다.

type AbsoluePath = string & { _brand: 'abs'};
function listAbsolutePath(path: AbsoluePath){
  console.log(path)
}
function isAbsolutePath(path: string) : path is AbsoluePath{
  return path.startsWith('/')
}

listAbsolutePath 는 절대 경로를 사용해 파일 시스템에 접근하는 함수이다. 런타임에는 절대경로로 시작하는지 체크하기 쉽지만 타입 시스템에서는 절대 경로를 판단하기 어려우므로 상표 기법을 사용한다.

string 타입이면서 _brand 속성을 가지는 객체를 만들 수 없고 AbsolutePath는 온전히 타입 시스템의 영역이다.

만약 path의 값이 절대 경로와 상대 경로 둘 다 될 수 있다면 타입 가드를 사용해 오류를 방지할 수 있다.

function f(path: string) {
  if(isAbsolutePath(path)){
    listAbsolutePath(path)
  }
  listAbsolutePath(path) 
}


<이펙티브 타입스크립트> Dan Vanderkam, 프로그래밍 인사이트 (2021)

profile
꾸준히 열심히!

0개의 댓글