[데보션영] 이펙티브 타입스크립트 도서 스터디 후기
데보션 영 활동에서 도서를 지원받아 <이펙티브 타입스크립트> 도서로 스터디를 진행하게 되었다.
스터디는 격주 금요일마다 대면으로 진행하였는데, 시간이 맞지 않는 날이 많아져서 이후에는 대부분 노션으로 스터디를 진행했었다.
스터디 진행 방식은 다음과 같다.
1. 아이템을 10개씩 나눠서 각자 맡은 부분을 정리하기
2. 만나서 서로에게 설명하기
정말 간단했다! 다만, 리마인드를 해야될 부분이나 중요한 부분의 경우 책 내용만을 갖고 하기보다는 예시 코드를 함께 작성하거나 결과를 출력해보는 형식으로 책의 내용을 활용할 수 있도록 노력했다.
다음은 내용을 정리한 것의 일부분이다.
아이템 36 : 해당 분야의 용어로 타입 이름 짓기
- 타입 이름 짓기가 중한 이유
- 엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드와 타입의 추상화 수준을 높여줌
- 잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어줌
해당 코드의 문제점interface Animal { name: string; endangered: boolean; habitat: string; } const leopard: Animal = { name: 'Snow Leopard', endangered: false, habitat: 'tundra', };
- name은 일반적인 용어이다.
- endangered 속성이 멸종 위기를 표현하기 위한 것인지 모호하다. 이미 멸종된 동물을 true로 해야하는가?
- habitat 속성은 범위가 너무 넓고 서식지라는 뜻이 불분명하다.
- 객체의 변수명이 leopard이지만 name값은 Snow Leopard이다. 객체의 이름과 속성의 name이 다른 의도인지 불분명하다.
개선한 점interface Animal { commonName: string; genus: string; species: string; status: ConservationStatus; climates: KoppenClimate[]; } type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC'; type KopprnClimate = | 'Af' | 'Am' | 'As' | 'Aw' | 'BSh' | //... 생략 ; const snowLeopard: Animal = { commonName: 'Snow Leopard;, genus: 'Panthera', species: 'Uncia', status: 'VU', climates: ['ET', 'EF', 'Dfd'], };
- name 을 commonName, genus, species 등 더 구체적인 용어로 대체했다
- endangered는 동물 보호 등급에 대한 분류 체계인 ConservationStatus 타입의 status로 변경
- habitat은 기후를 뜻하는 climates로 변경
타입, 속성, 변수에 이름을 붙일 때 명심해야 하는 규칙
- 동일한 의미를 나타낼 때는 같은 용어를 사용해야 한다.
- data, info, thing, item, object 등 모호하고 의미 없는 이름은 피해야 한다.
- 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지 고려해야 한다.
- ex ) INodeList 보다는 Directory사 더 의미 있음.
아이템 37 : 공식 명칭에는 상표를 붙이기
interface Vector2D {
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);
⇒ 수학적으로 2차원 벡터를 사용해야 이치에 맞음
- 3차원 벡터를 허용하지 않게 하기 위해 공식 명칭을 사용 즉, ‘상표(brand)’를 붙인다.
- 해당 기법은 런타임에 상표를 검사하는 것과 같은 효과를 얻을 수 있음
- 런타임 오버헤드를 줄일 수 있음
- 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입도 상표화 가능
- interface Vector2D { _brand: '2d'; // Vector2D 타입만 받는 것을 보장함. x: number; y: number; } 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}; calculateNorm(vec3D);
절대 경로를 사용해 파일 시스템에 접근하는 함수
절대 경로로 시작하는지 체크하는 것은 쉽지만, 타입 시스템에서는 절대 경로를 판단하기 어렵기 때문에 상표 기법을 사용함
type AbsolutePath = string & {_brand: 'abs'};
function listAbsolutePath(path: Absolute) {
//...
}
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startWith('/');
}
- string 타입이면서 _brand 속성을 갖는 객체는 만들 수 없음
- AbsolutePath는 온전히 타입 시스템의 영역
path 값이 절대 경로와 상대 경로 둘 다 될 수 있다면 타입을 정제해 주는 타입 가드를 사용해 오류를 방지할 수 있음
function f(path: string) {
if (isAbsolutePath(path)) {
listAbsolutePath(path);
}
listAbsolutePath(path);
// ~~~ 'string' 형식의 인수는 'AbsolutePath' 형식의 매개변수에 할당될 수 없습니다.
오류 해결 방법
- path as AbsolutePath 사용
- → 하지만 단언문은 지양해야 함
- AbsolutePath 타입을 매개변수로 받거나 타입이 맞는지 체크
속성 모델링에 사용
function binarySearch<T>(xs: T[], x: T) boolean {
let low = 0, high = xs.length - 1;
while (high >= low) {
const mid = low + Math.floor((high - low) / 2);
const v = xs[mid];
if (v === x) return true;
[low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
}
return false;
}
목록이 정렬되어 있다는 의도를 표현하기 어렵기 때문에 상표 기법 사용
type SortedList<T> = T[] & {_brand: 'sorted'};
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;
}
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
//...
}
- binarySearch를 호출하기 위해 상표가 붙은 SortedList 타입의 값을 사용하거나 isSorted를 호출해 증명해야 함.
- ⇒ 효율적이진 않지만 안전성은 확보할 수 있음.
아이템 38 : any 타입은 가능한 한 좁은 범위에서만 사용하기
any 사용법
function processBar(b: Bar) {/*...*/}
function f() {
const x = expressionReturningFoo();
processBar(x);
// ~ 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}
오류를 제거하는 방법
function f1() {
const x: any = expressionReturningFoo();
processBar(x);
}
function f2() {
const x = expressionReturningFoo();
processBar(x as any);
}
- f1() < f2()
- any 타입이 processBar 함수의 매개변수에서만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않음.
- f1에서는 함수의 마지막까지 x의 타입이 any지만 f2에서는 processBar 호출 이후 x가 그대로 Foo 타입임.
any를 사용하지 않고 오류를 제거할 수 있는 법 : @ts-ignore 사용
function f1() {
const x = expressionReturningFoo();
// @ts-ignore
processBar(x);
return x;
}
타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우
함수의 반환 타입을 명시하는 것이 좋음.
→ ant 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있다.
객체 관련 any 사용법
어떤 큰 객체 안의 한 개 속성이 타입 오류를 갖는 상황
const config: Config = {
a: 1,
b: 2,
c: {
key: value
// ~~~ 'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없습니다.
}
};
- 해결 방법
- config 객체 전체를 as any로 선언→ 즉, 최소한의 범위에만 any를 사용하는 것이 좋다!
- const sonfig: Config = { a: 1, b: 2, c: { key: value as any } };
- → 다른 속성들 역시 타입 체크가 되지 않는 부작용이 생김
요약
- 의도치 않은 타입 안전성의 손실을 피하기 위해 any 사용 범위를 최소화 해야한다.
- any 타입은 반환하면 절대 안된다.
도서 스터디를 진행하며 타입스크립트에 대한 이해를 더 높일 수 있었던 것 같다. 특히나 다른 분들과 함께 스터디를 진행하다보니 혼자 공부할 때보다 더욱 높은 효율로 스터디를 진행하고 혼자 공부했을 땐 몰랐을 내용을 추가로 알게되었던 것 같다.