2022년 12월 9일 글입니다.
Item 22. 타입 좁히기
✔ 요약 1. 타입 좁히기를 이해하자
✔ 요약 2. 분기문, 태그된/구별된 유니온, 사용자 정의 타입가드등 스킬을 익히자
- 가장 기본적인 null 체크
const el = document.getElementById('foo'); // HTMLElement | null
if(el){ //HTMLElement
el
el.innerHTML = "party".blink();
}else{ //null
el
alert("no element #foo")
}
위 코드는 HTMLElement | null 인 el 의 타입을 if 분기문을 통해서 HTMLElement로 좁힌다.
- instanceof를 이용한 타입좁히기(narrowing)
function contains(text: string, search: string|RegExp) {
if (search instanceof RegExp) {
search // Type is RegExp
return !!search.exec(text);
}
search // Type is string
return text.includes(search);
}
위 코드는 instanceof 가 어떻게 타입인 RegExp를 구분하는 거지? instanceof는 런타임에서 동작하는데?? 라는 의문이 들어서 RegExp를 찾아보니 클래스였다 >< (클래스는 런타임도 , 빌드타임도 동작함)
- 속성체크를 이용한 타입 좁히기
interface A { a: number }
interface B { b: number }
function pickAB(ab: A | B) {
if ('a' in ab) {
ab // Type is A
} else {
ab // Type is B
}
ab // Type is A | B
}
in은 값이 있는지 비교하기 때문에 타입에 상관없이 타입을 좁힐 수 있다.
- isArray와 같은 내장함수로 타입 좁히기
function contains(text: string, terms: string|string[]) {
const termList = Array.isArray(terms) ? terms : [terms];
termList // Type is string[]
// ...
}
- 주의할 점
const el = document.getElementById('foo'); // type is HTMLElement | null
if (typeof el === 'object') {
el; // Type is HTMLElement | null
}
위와 같은 방법은 잘못된 방식인데, 자바스클비트에서 typeof null 또한 "object"이기 때문에, object인지 검사하는 방식으로는 타입을 좁힐 수 없다.
function foo(x?: number|string|null) {
if (!x) {
x; // Type is string | number | null | undefined
}
}
빈문자열 ''와 0 모두 falsy한 값이라 false로 판명되어 타입은 좁혀지지 않는다.
- 태그된 유니온 / 구별된 유니온
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch (e.type) {
case 'download':
e // Type is DownloadEvent
break;
case 'upload':
e; // Type is UploadEvent
break;
}
}
개발자가 의도적으로 type이란 태그를 붙이는 거다. 의도적으로 타입이란 속성을 만들어 붙여줌으로써, 유니온값을 구별할 수 있어진다.
- 사용자 정의 타입가드
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael'];
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
const members = ['Janet', 'Michael'].map(
who => jackson5.find(n => n === who)
).filter(isDefined); // Type is string[]
일부러 만든 isDefined라는 사용자정의타입가드를 이용해 타입을 구분한다.
그냥 filter에 who => who!== undefined 를 한다고 해서 undefined 가 좁혀지지는 않기 때문이다.
좁혀지지 않는 이유는 filter함수의 반환값이 undefined가 나올 수 있기 때문
그러니 함수를 이용해 isDefined를 사용해주자.
단 이 타입가드는 undefined가 아니라는 확식이 들 때만 사용할 수 있을 것 같다.
Item 23. 한꺼번에 객체 생성하기
요약 1. 왠만하면 한꺼번에 객체를 생성해라.
요역 2. 객체를 따로 만들어야 한다면 타입 단언문 (as) 나 전개연산자 (...)를 사용해라.
타입스크립트의 타입은 일반적으로 변경되지 않는다는 걸 가정한다. 그래서 왠만하면 객체 생성과 프로퍼티 할당을 한번에 해라
즉 이렇게 하지 말란 소리다
const pt = {}
pt.x = 3;
pt.y = 4 ; // '{}' 형식에 x 와 y값이 없어서 오류가 난다.
const pt={
x = 3,
y = 4
} //직관적이고 코드도 쉽다.
‼하지만 굳이 객체를 나눠서 만들어야 한다면 아래와 같은 방법을 사용하자.
- as 타입단언문 사용하기
const pt ={} as Point; pt.x = 3 pt.y = 4
as 단언문으로 미리 타입을 좁혀주면 에러가 나지 않는다.
```typescript
interface Point { x: number; y: number; }
const pt0 = {};
const pt1 = {...pt0, x: 3};
const pt: Point = {...pt1, y: 4}; // OK
이런식으로 (이렇게 짜는 사람이 있을까?)
전개연산자를 객체의 타입 선언및 생성을 해준다.
- 객체이 조건부로 속성을 추가하기
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
const president: {
middle: string;
first: string;
last: string;
} | {
first: string;
last: string;
}
- 앞에서 배운 헬퍼함수를 이용할 수도 있다.
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
function addOptional<T extends object, U extends object>(
a: T, b: U | null
): T & Partial<U> {
return {...a, ...b};
}
const president = addOptional(firstLast, hasMiddle ? {middle: 'S'} : null);
president.middle // OK, type is string | undefined
Item 24. 일관성 있는 별칭 사용하기
요약 1. 별칭 (alias)는 타입좁히는 걸 방해한다. 일관성있게 별칭을 사용하자.
요약 2. 비구조화 할당을 이용해서 일관된 이름을 사용하자
요약 3. 함수 호출이 객체 속성 타입 정제를 무효화 할 수 있다. 하지만 타입스크립트는 무효화 하지 않는다는걸 가정한다. 그러니 속성보다 지역변수를 사용해 타입정제를 하자.
interface Coordinate {
x: number;
y: number;
}
interface BoundingBox {
x: [number, number];
y: [number, number];
}
interface Polygon {
exterior: Coordinate[];
holes: Coordinate[][];
bbox?: BoundingBox;
}
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
const box = polygon.bbox;
if (box) {
if (pt.x < box.x[0] || pt.x > box.x[1] ||
pt.y < box.y[1] || pt.y > box.y[1]) { // OK
return false;
}
}
// ...
}
위 처럼하면 polygon은 bbox인데, const box 라면서 코드를 읽는 사람에게 오류가 발생한다.
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
const {bbox} = polygon;
if (bbox) {
const {x, y} = bbox;
if (pt.x < x[0] || pt.x > x[1] ||
pt.y < x[0] || pt.y > y[1]) {
return false;
}
}
// ...
}
이런식으로 객체 비구조화를 사용하면 네이밍도 같아지므로 편안하다.
지역변수에서 잘 동작하지만, 객체 속성에서는 주의해야 하는데,
function fn(p: Polygon) { /* ... */ }
polygon.bbox // Type is BoundingBox | undefined
if (polygon.bbox) {
polygon.bbox // Type is BoundingBox
fn(polygon);
polygon.bbox // Type is still BoundingBox
}
이런 상황에선, fn을 통과하면서 undefined가 나올 수도 있는 상황인데, 이미 if문을 통과했다고 타입스크립트는 undefined가 나올거라고 예상하지 못한다.
- 타입스크립트는 함수가 타입 정제를 무효화 하지 않는다고 가정하기 때문에.
하지만 그렇다고 bbox를 뽑아서 쓰면 polygon.bbox와 다른 형태를 띌수도 있다.
주소 참조 vs 값 참조는 언제나 주의하자.
이런 오류가 일어날 수 있음을 알고 코딩하는 것과 모르고 코딩하는 것과는 다르니까 알아두자.
'Book Review > Effective Typescript' 카테고리의 다른 글
Day9_Effective Typescript 스터디 (0) | 2023.06.16 |
---|---|
Day8_Effective Typescript 스터디 (item 25~ 26) (0) | 2023.06.16 |
Day6_Effective Typescript 스터디 (0) | 2023.06.16 |
day5_EffectiveTypescript 스터디 (0) | 2023.06.16 |
Day4_EffectiveTypescript 스터디 (0) | 2023.06.16 |