Effective Typescript (Day 9)

d_fe·2023년 1월 3일
post-thumbnail

item23. 한꺼번에 객체 생성하기

변수의 값은 변경될 수 있지만 타입은 일반적으로 변경되지 않는다.
이런 특성을 이용하여, 객체를 생성할 때는 속성을 하나씩 추가하기보다 여러 속성을 포함해서 한꺼번에 생성해야 타입추론에 유리하다.

const pt = {};
pt.x = 3;
// ~ '{}' 형식에 'x' 속성이 없습니다.
pt.y = 4;
// ~ '{}' 형식에 'y' 속성이 없습니다.

첫 줄의 pt 타입은 {} 값을 기준으로 추론되기 떄문에 존재하지 않는 속성을 추가할 수 없다.

interface Point {x: number; y: number;}
const pt: Point = {};
// ~~ '{}' 형식에 'Point' 형식의 x, y 속성이 없습니다.
pt.x = 3;
pt.y = 4;

Point 인터페이스를 정의하면 다른 부분에 여전히 오류가 발생한다.
이는 객체를 한번에 정의하면 해결할 수 있다.

const pt = {
	x: 3,
  	y: 4,
}

// 객체를 반드시 제각각 나눠서 만들어야 한다면 단언문을 사용한다.
const pt = {} as Point;
pt.x = 3;
pt.y = 4;

// 이 경우에도 한꺼번에 만드는 게 좋다.
const pt: Point = {
	x: 3,
  	y: 4,
}

➡️ 작은 객체들을 조합해서 큰 객체를 만들어야 하는 경우에는 객체 전개 연산자를 사용한다.

const pt = {x:3, y:4};
const id = {name: 'Patdfdf'};
const namedPoint = {};
Object.assign(namedPoint, pt, id);
namedPoint.name; // 형식에 'name'속성 없음 오류

// 객체 전개 연산자 이용
const namedPoint = {...pt, ...id};
namedPoint.name; // 정상, 타입은 string

객체 전개 연산자를 사용하면 타입 걱정 없이 필드 단위로 객체를 생성할 수도 있다.
이때 모든 업데이트마다 새 변수를 사용하여 각각 새로운 타입을 얻도록 하는 게 중요하다.

const pt0 = {}:
const pt1 = {...pt0, x:3};
const pt: Point = {...pt1, y: 4};

➡️ 타입에 안전한 방식으로 조건부 속성을 추가하려면, 속성을 추가하지 않는 null 또는 {}으로 객체 전개를 사용한다.

declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};

전개 연산자로 한꺼번에 여러 속성을 추가할 수도 있다.

declare let hadDates: boolean;
const nameTitle = {name: 'Khufu', title: 'Pharaoh'};
const pharaoh = {
	...nameTitle,
  	...(hasDates ? {start : 1213, end: 1532} : {})
}

이제는 타입이 유니온으로 추론된다.

const pharaoh: {
	start: number;
  	end: number;
  	name: string;
  	title: string;
} | {
	name: string;
  	title: string;
}

start와 end가 항상 함께 정의되기 때문에 위와 같이 나타난다.
유니온을 사용하는 게 가능한 값의 집합을 더 정확히 표현할 수 있지만, 유니온보다 선택적 필드가 다루기에 더 쉬울 수 있다.
선택적 필드 방식으로 표현하려면 다음처럼 헬퍼 함수를 사용하면 된다.

function addOptional<T extends object, U extends object>(a: T, b: U | null): T Partial<U> {
	return {...a, ...b};
}

const pharaoh = addOptional(
	nameTitle,
  	hasDates ? {start : 1231, end: 5151} : null
);
pharaoh.start // 타입은 number | undefined

가끔 객체나 배열을 변환해서 새로운 객체나 배열을 생성하고 싶다면 루프 대신 내장된 함수형 기법 또 로대시(Lodash) 같은 유틸 라이브러리를 사용하는 것이 옳을 것이다.


item24. 일관성 있는 별칭 사용하기

const bbo = {name: 'Brooklyn', location:[40314,141]};
const loc = bbo.location;

> loc[0] = 0;
> bbo.location // [0,141]

별칭의 값을 변경하면 원래 속성값에서도 변경된다. (주소 참조)
별칭을 남발해서 사용하면 제어 흐름을 분석하기 어렵다.

interface Coordinate {
   x: number;
   y: number;
}

interface BoundingBox {
   x: [number, number];
   y: [number, number];
}

interface Polygon {
   exterior: Coordinate[];
   holes: Coordinate[];
   bbox?: BoundingBox
}

// bbox는 필수가 아닌 최적화 속성
// 어떤 점이 다각형에 포함 되는지 체크

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) {
   if(polygon.bbox) {
       if(pt.x < polygon.bbox.x[0] || pt.x > polygon.bbox.x[1] ||
           pt.y < polygon.bbox.y[0] || pt.y > polygon.bbox.y[1]) {
           return false;
       }
   }
}

// 위 코드의 반복되는 부분을 없애기 위해 임시 변수를 뽑아낸다.
function isPointInPolygon (polygon: Polygon, pt: Coordinate) {
   const box = polygon.bbox
   if(polygon.bbox) {
       if(pt.x < box.x[0] || pt.x > box.x[1] ||
           pt.y < box.y[0] || pt.y > box.y[1]) {
           return false;
       }
   }
} // box 객체가 undefined일 수 있다는 오류가 발생한다.
// 제어 흐름 분석을 방해했기 때문

위와 같은 오류는 별도의 별칭을 만듦으로 인해 제어 흐름 분석을 방해했기 때문에 발생한다.
이러한 오류는 '별칭은 일관성 있게 사용한다' 는 기본 원칙을 지키면 방지할 수 있다.
(조건문의 polygon.bbox 를 box로)

하지만 여전히 box와 bbox는 같은 값인데도 다른 이름을 사용했기 때문에 읽는 데 불편함을 준다.
이 때 객체 비구조화를 이용하면 간결한 문법으로 일관된 이름을 사용할 수 있다.

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 < y[0] || pt.y > y[1]) {
            return false;
        }
    }
}

bbox 속성이 아니라 x와 y가 선택적 속성일 경우에는 속성 체크가 더 필요하다. 따라서 타입의 경계에 null 체크를 해주는 것이 좋다.

➡️ 별칭은 타입 체커뿐 아니라 런타임에도 혼동을 야기할 수 있다.

const {bbox} = polygon;
if (!bbox) {
	calculatePolygonBbox(polygon); //polygon.bbox가 채워진다.
  	// bbox가 채워졌으니 이제 bbox와 polygon.bbox 는 다른 값을 참조한다.
}

➡️ 타입스크립트의 제어 흐름 분석은 지역 변수에는 꽤 잘 동작하나 객체 속성에서는 주의해야 한다.

function fn(p: Polygon) {...}

polygon.bbox 	 // 타입은 BoundingBox | undefined
if(polygon.bbox) {
	polygon.bbox // 타입은 BoundingBox
  	fn(polygon)
  	polygon.bbox // 타입은 BoundingBox
}

fn(polygon) 호출이 polygon.bbox를 제거할 가능성이 있으므로 타입이 undefined를 포함하는 것이 안전할 것이나, 그렇게 되면 매번 함수를 호출할 때마다 속성 체크를 반복해야 한다.

따라서 타입스크립트는 함수가 타입 정제를 무효화하지 않는다고 가정한다.
but, 무효화될 가능성의 예시로 polygon.bbox로 사용하는 대신 bbox 지역 변수로 뽑아내서 사용하면 bbox의 타입은 유지되지만, polygon.bbox의 값과 같게 유지되지 않을 수 있다.

☑️ 별칭은 타입스크립트가 타입을 좁히는 것을 방해한다. 변수에 별칭을 사용할 땐 일관되게 사용할 것
☑️ 비구조화 문법이 위의 예시.
☑️ 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점을 인지한다. 속성보다 지역 변수를 사용하면 타입 정제를 믿을 수 있다.

profile
오늘보다 내일 더 성장하는 프론트엔드 개발자가 되기 위해

0개의 댓글