이펙티브 타입스크립트 (댄 밴더캄 지음) 를 읽고 정리
📍요약
✅타입 안전성에서 불쾌한 골짜기(uncanny valley)는 피하는 것이 좋다
타입이 없는 것보다 잘못된게 더 나쁘다
- 불쾌한 골짜기 : 어설프게 완벽을 추구하다가 역효과가 나는 것
- 로봇공학, 인공지능 분야에서 어설프게 완벽을 추구하는 로봇, 인공지능에서 느껴지는 불쾌함에서 유래
✅타입 정보를 구체적으로 만들 수록 언어 서비스(오류 메시지와 자동 완성)도 신경써야 함
언어 서비스는 개발 경험에 중요
📍추상적인 타입을 구체화하기
- 경도 (column), 위도 (row) 정보를 타입 모델링
- 좌표를 number[] 대신 구체적으로 튜플 [number, number] 로 표현했다
type GeoPosition = [number, number];
interface Point {
type: 'Point';
// coordinates: number[]; -> 너무 추상적이므로 튜플로 개선
coordinates: GeoPosition;
}
interface LineString {
type: 'LineString';
coordinates: GeoPosition[];
}
interface Polygon {
type: 'Polygon';
coordinates: GeoPosition[][];
}
type Geometry = Point | LineString | Polygon;
- 그런데, 이는 마냥 좋은 것만은 아니다
- 위도와 경도 대신 고도를 입력해야 한다면? 또 다시 수정이 필요하기 때문
📍Mapbox 라이브러리 예시
]연산자, 매개 변수들] 형태의 연산을 모델링하려고 한다
/*
["+", 1, 2] // 3
["/", 20, 2] // 10
["case", [">", 20, 10], "red", "blue"] // "red"
["rgb", 255, 0, 127] // "#FF007F"
*/
가능한 입력값의 전체 종류는 아래와 같다
1. 모두 허용
2. 문자열, 숫자, 배열, 허용
3. 문자열, 숫자, 알려진 함수 이름으로 시작하는 배열 허용
4. 각 함수가 받는 매개변수의 갯수가 정확한지 확인
5. 각 함수가 받는 매개변수의 타입이 정확한지 확인
✅1번, 2번에 대한 타입을 선언하고 테스트해보면..
type Expression1 = any;
type Expression2 = number | string | any[];
const tests: Expression2[] = [
10,
"red",
true, // true가 들어온 것을 걸러냄
// ~~~ Type 'true' is not assignable to type 'Expression2'
["+", 10, 5],
["case", [">", 20, 10], "red", "blue", "green"], // Too many values
["**", 2, 31], // Should be an error: no "**" function
["rgb", 255, 128, 64],
["rgb", 255, 0, 127, 0] // Too many values
];
정밀도를 끌어올리기 위해 튜플의 첫 번째 요소에 문자열 리터럴 타입의 유니온 사용
-> 허용되지 않은 "**" (제곱) 을 거르기 위해서
type Expression1 = any;
type Expression2 = number | string | any[];
// 가능한 연산을 문자열 리터럴 유니온 타입으로 정의
type FnName = '+' | '-' | '*' | '/' | '>' | '<' | 'case' | 'rgb';
type CallExpression = [FnName, ...any[]];
type Expression3 = number | string | CallExpression;
const tests: Expression3[] = [
10,
"red",
true,
// ~~~ Type 'true' is not assignable to type 'Expression3'
["+", 10, 5],
// 20 > 10 true 이면 red, 아니면 blue 인데, green이 갑자기 추가됨
["case", [">", 20, 10], "red", "blue", "green"],
["**", 2, 31],
// ~~~~~~~~~~~ Type '"**"' is not assignable to type 'FnName'
["rgb", 255, 128, 64]
];
⭐"**" 연산을 거를 수 있게 됨!
- 정밀도를 유지하면서 오류를 하나 더 잡음
예제의 모든 에러를 탐지하기 위해 더 구체적으로 설정
type Expression1 = any;
type Expression2 = number | string | any[];
type Expression4 = number | string | CallExpression;
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 // etc.
}
interface RGBCall {
0: 'rgb';
1: Expression4;
2: Expression4;
3: Expression4;
length: 4;
}
const tests: Expression4[] = [
10,
"red",
true,
// ~~~ Type 'true' is not assignable to type 'Expression4'
["+", 10, 5],
["case", [">", 20, 10], "red", "blue", "green"],
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '["case", [">", ...], ...]' is not assignable to type 'string'
["**", 2, 31],
// ~~~~~~~~~~~~ Type '["**", number, number]' is not assignable to type 'string
["rgb", 255, 128, 64],
["rgb", 255, 128, 64, 73]
// ~~~~~~~~~~~~~~~~~~~~~~~~ Type '["rgb", number, number, number, number]'
// is not assignable to type 'string'
];
- 모든 에러 탐지 가능!
- 하지만 에러 메시지가 오히려 더 부정확해짐 (언어 서비스를 사용할 수 없어지므로 오히려 더 안좋아짐)
⭐너무 완벽한 타입 정의를 좇을 필요가 없다