[이펙티브 타입스크립트] 아이템 34 - 부정확한 타입보다는 미완성 타입을 사용하기

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

😖잘못된 타입을 쓸 바엔 차라리 타입이 없는 것이 낫다!

  • 정확하게 타입 모델링 할 수 없다면 부정확하게 모델링하지 말아야 한다.
  • 타입 선언을 세밀하게 만들고자 했지만 오히려 타입이 부정확해질 수 있다.
    • 타입 선언을 변경시키지 않으려면 타입 단언문 도입 혹은 as any 추가해서 타입 체커 무시
/** 추상적으로 선언 */
interface Point {
	type: 'Point';
	coordinates: number[];     // 추상적이다! [1], [1,2], [1,2,3,4, ...] 가능
}

/** 좀 더 명확하게 튜플 타입으로 선언 */
type GeoPosition = [number, number]; // 위도와 경도 명시
interface Point {
	type: 'Point';
	coordinates: GeoPosition;  // 구체적이지만 또 다른 정보(예-고도)를 고려하지 않았다.
}

🔍 튜플 타입이란? 길이와 각 요소마다 타입이 고정된 배열을 의미한다.

👀맵박스(Mapbox) 라이브러리로 구체적 타입이 별로라는 것을 확인해보자

  1. 모두 허용
type Expression1 = any;
  1. 문자열, 숫자, 배열 허용
    • 타입을 구체적으로 만들수록 정밀도가 손상되는 것을 방지하는데 도움이 된다.
type Expression2 = number | string | any[];

const tests: Expression2[] = [
	10,
	"red",
	true, // 오류!! boolean은 Expression2에 해당되지 않음.
	["+", 10, 5],
	["case", [">", 20, 10], "red", "blue", "green"], // 오류가 발생하지 않음. (개수 초과)
	["**", 2, 31], // 오류가 발생하지 않음. ("**"는 함수가 아님.)
	["rgb", 255, 128, 64],
	["rgb", 255, 0, 127, 0] // 오류가 발생하지 않음. (개수 초과)
];
  1. 문자열, 숫자, 알려진 함수 이름으로 시작하는 배열 허용
    • 정밀도를 유지하면서 오류를 발견.
    • 아직 매개변수 개수를 파악하지 못하는 문제점.
type FnName = '+' | '-' | '*' | '/' | '>' | '<' | 'case' | 'rgb';
type CallExpression = [FnName, ...any[]];
type Expression3 = number | string | CallExpression;

const tests: Expression3[] = [
	10,
	"red",
	true, // 오류!! boolean은 Expression3에 해당되지 않음.
	["+", 10, 5],
	["case", [">", 20, 10], "red", "blue", "green"], // 오류가 발생하지 않음. (개수 초과)
	["**", 2, 31], // 오류!! "**"는 FnName에 해당되지 않음.
	["rgb", 255, 128, 64],
	["rgb", 255, 0, 127, 0] // 오류가 발생하지 않음. (개수 초과)
];
  1. 각 함수가 받는 매개변수의 개수가 정확한지 확인
    • 타입 정보가 더 정밀해졌지만 개선되었다고 보기에는 어렵다.
    • 메시지도 부정확해진다. → 실제로는 크게 잘 모르겠어요! 이전 경우와 비슷합니다.
    • 타입 선언의 복잡성으로 인해 버그 발생 가능성이 높아졌다.
      • 고려해야 할 요소도 더 늘어나면 이제 환장하는 거지요^_^
type Expression4 = number | string | CasllExpression;
type CallExpression = MathCall | CaseCall | RGBCall;

interface MathCall {
	0: '+' | '-' | '*' | '/' | '>' | '<';
	1: Expression4;
	2: Expression4;
	length: 3;
}

interface CaseCall {
	0: 'case'
	1: Expression4;
	2: Expression4;
	3: Expression4;
	length: 4 | 6 | 8 | 10 | 12 | 14 | 16
}

interface RGBCall {
	0: 'rgb';
	1: Expression4;
	2: Expression4;
	3: Expression4;
	length: 4;
}

const tests: Expression4[] = [
	10,
	"red",
	true, // 오류!! boolean은 Expression3에 해당되지 않음.
	["+", 10, 5],
	["case", [">", 20, 10], "red", "blue", "green"], // 오류!! 개수가 초과되었음.
	["**", 2, 31], // 오류!! "**"는 FnName에 해당되지 않음.
	["rgb", 255, 128, 64],
	["rgb", 255, 0, 127, 0] // 오류가 발생하지 않음. (개수 초과)
];

text 위의 예시대로 입력했을 때 실제로 생기는 에러 메시지들

😎그래서 결론은 너무 구체적인 타입은 별로라는 것!

  • 부정확함을 바로잡기 위해 코드를 정밀하게 만들려다가 코드가 오히려 부정확해질 수 있다.
  • 타입이 점점 복잡해지면서 버그 발생 가능성이 높아진다.
  • 부정확함을 바로잡는 방법 대신 테스트 세트를 추가해 놓친 부분이 없는지 확인해도 된다.

💡결론

  • 타입이 없는 것보다 잘못된 게 더 나쁘다. 그러니 정확하게 모델링 못한다면 아예 하지 말자.
  • 타입 정보를 구체적으로 만들수록 오류 메시지와 자동 완성 기능에 주의를 기울이자.
profile
프론트엔드를 공부하고 있는 학생입니다🐌

0개의 댓글