타입스크립트의 타입 시스템 [3]

DOHEE·2022년 11월 16일
0

EffectiveTypescript

목록 보기
4/11

[9] 타입 연산과 제너릭 사용으로 반복 줄이기

DRY(don't repeat yourself) 원칙은 같은 코드를 반복하지 말라는 원칙을 의미한다.

반복되는 코드를 최대한 제거하여 가독성을 높고 유지보수가 편리한 코드를 짜야한다는 것을 의미한다.

하지만, 이 원칙을 잘 지키려고 노력하는 사람들도 놓치기 쉬운 부분이 타입에 대한 반복이다.

타입 중복을 피하는 방법에는 여러 가지가 있다.

1. 타입에 이름 붙이기

가장 간단한 방법은 타입에 이름을 붙이는 것이다. 상수를 사용해서 반복을 줄이는 기법을 동일하게 타입 시스템에 적용한 것이다.

// 이름 지정 X
function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
	return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}

// 이름 지정 O
interface Point2D {
	x: number;
    y: number;
}
function distance(a: Point2D, b: Point2D){
	return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}

2. 타입 확장하기

같은 요소를 공유하는 타입이 있다면 공통을 부분에서 확장하는 방식으로, 한 타입이 다른 타입의 부분 집합이라면 일방향으로 확장하는 식으로 타입 중복을 해결할 수 있다.

interface Person {
	firstName: string;
    lastName: string;
}

// 확장 전
interface PersonWithBirthDate {
	firstName: string;
    lastName: string;
    birth: Date;
}

// 확장 후
interface PersonWithBirthDate extends Person {
	birth: Date;
}
  1. 전체의 부분집합으로 타입 사용하기
    타입 확장이 공통된 부분을 기준으로 넓혀가는 방식이라면 전체 요소를 가진 타입을 활용하는 방법도 있다.
interface Person {
	firstName: string;
    lastName: string;
    age: number;
    birth: Date;
    school: string;
    phoneNumber: string;
}

type UserInfoForRegister = {
	firstName: Person['firstName'];
	lastName: Person['lastName'];
  	phoneNumber: Person['phoneNumber'];
}

마치 객체에서 key값을 기준으로 값을 가져오듯이 값을 넣어줄 수 있다. 이런 경우 Person에서 타입을 변경하면 자동으로 UserInfoForRegister에서도 타입이 변경되어 관리가 훨씬 용이하다.

앞선 예제를 보다 반복을 줄이자면 매핑을 사용하여 다음과 같이 변경할 수 있다.

type UserInfoForRegister = {
  	[k in 'firstName' | 'lastName' | 'phoneNumber']: Person[k];
}

4. Partial : 선택적 타입

아래와 같이 업데이트에 대한 타입의 경우 update 메서드의 매개변수 타입은 기본 타입과 요소는 동일하면서도 타입 대부분이 선택적 필드가 되어야 한다.

interface PersonInfo { 
	firstName: string;
  	lastName: string;
  	age: number
} 
interface PersonInfoUpdate { 
	firstName: string;
  	lastName: string;
  	age: number
} 

이 때 매핑된 타입과 keyof를 사용하면 불필요한 중복을 피할 수 있다. keyof는 속성 타입의 유니온을 반환한다.

type OptionsUpdate = {[k in keyof Options]? : Options[k]} 

5. typeof

값의 형태에 해당하는 타입을 정의하고 싶을 때는 typeof를 사용하면 된다.

const PersonInfo = { 
	firstName: string;
  	lastName: string;
  	age: number
} 
type Person = typeof PersonInfo;

[10] 동적 데이터에 인덱스 시그니처 사용하기

타입스크립트에서는 타입에 '인덱스 시그니처'를 명식하여 유연하게 매핑을 표현할 수 있다.

type Rocket = {[property: string]: string};

키의 이름은 중요하지 않으나 키의 타입은 string이나 number, symbol의 조합이어야 한다. 값은 무엇이든 가능하다.

굉장히 편리한 기능이지만 단점도 존재한다.

1) 잘못된 키를 포함해 모든 키를 허용한다.
2) 특정 키가 필요하지 않다.
3) 키마다 다른 타입을 가질 수 없다.
4) 타입스크립트 언어 서비스를 이용할 수 없다.

따라서, 자주 사용하는 것은 추천하지 않는다. 하지만 키를 알 수 없는 상황에서는 굉장이 유용한 기능일 것이다.

[11] number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기

앞선 주제에서처럼 인덱스 시그니처에서 키의 타입에는 number가 들어갈 수 있다. 하지만 키의 타입이 number로 지정하는 것은 추천하지 않는다.

자바스크립트로 실행할 경우 key값을 숫자를 지정하더라도 string 값으로 변환되어 객체에 들어간다. 따라서 number로 지정하는 것이 큰 의미가 없다.

또한, 타입스크립트를 조금 실행해 본 사람이라면 알고 있겠지만 배열의 타입은 object로 나온다.

객체의 key값이 숫자가 되지 않는다면 어떻게 array[0]이 가능할까? array['0']도 동일한 결과가 나온다.

즉, 인데스 시그니처가 number로 표현되더라도 실제 런타임에 사용되는 키는 string이다.

[12] 변경 관련된 오류 방지를 위해 readonly 사용하기

readonly는 읽기만 허용한다는 의미이다. 값을 변경하지는 않는 것이 핵심이다.

매개변수를 readonly로 선언하면 다음과 같은 일이 생긴다.

1 ) 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다.
2 ) 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다.
3 ) 호출하는 쪽에서 함수에 readonly 배열ㅇ르 매개변수로 넣을 수 있다.

readonly를 사용하면 인터페이스를 명확히 하고 타입 안전성을 높일 수 있기 때문에 단점이 많지 않다.

readonly인 배열을 활용하는 방법에는 여러가지가 있겠지만 크게 concat을 활용하는 방법과 복사본을 만드는 방법이 있다.

concat은 원본을 변경하지 않고 새로운 배열을 반환하는 메소드이다. 또한, 얕은 복사를 활용하여 복사본을 만들 수도 있다.

readonly는 얕게 작용하기 때문에 단언문을 사용하면 readonly가 풀린다. 깊은 readonly는 제네릭을 만들어 구현할 수 있지만 매우 까다로우므로 라이브러리를 사용하는 것을 추천한다.

[13] 매핑된 타입을 사용하여 값을 동기화하기

만약 산점도 그래프를 그려야한다고 가정해보자.

데이터가 변화하면 그에 맞춰서 그래프를 다시 그려야한다.

interface ScatterProps {
	xs: number[];
    ys: number[];
    
    xRange: [number, number];
    yRange: [number, number];
    color: string;
    
    onClick: (x: number, y: number, index: number) => void;
}

최적화 하는 방식에는 실패에 열린 접근법과 실패에 닫힌 접근법이 있다.

1) 실패에 닫힌 접근법 ( 보수적 접근법 )

function shouldUpdate (
	oldProps: ScatterProps,
    newProps: ScatterProps
) {
	let k: keyof ScatterProps;
    for (k in oldProps) {
    	if (oldProps[k] !== newProps[k]) {
        	if (k !== 'onClick') return true;
        }
    }
    return false;
}

새로운 속성이 추가되면 shoulUpdate 함수는 값이 변경될 때마다 차트를 다시 그린다. 이 접근법을 이용하면 차트가 정확하지만 너무 자주 그려질 가능성이 있다.

2 ) 실패에 열린 접근법

function shouldUpdate (
	oldProps: ScatterProps,
    newProps: ScatterProps
) {
	return (
    	oldProps.xs !== newProps.xs ||
        oldProps.xy !== newProps.xy ||
        oldProps.xRange !== newProps.xRange ||
        oldProps.yRange !== newProps.yRange ||
        oldProps.color !== newProps.color
    );
}

차트를 불필요하게 다시 그리는 단점을 해결했지만 실제로 다시 그려야 할 경우에 누락된느 일이 생길 수 있다. 이는 '우선, 망치지 말 것(first, do no harm)'을 어기기 때문에 일반적으로 사용하는 방식은 아니다.

따라서 새로운 속성이 추가될 때, 직접 shouldUpdate를 고치도록 하는 것이 낫다. 하지만 이를 타입 체커가 동작할 수 있도록 고치는 것이 베스트이다.

profile
안녕하세요 : ) 천천히라도 꾸준히 성장하고 싶은 개발자 DOHEE 입니다! ( 프로필 이미지 출처 : https://unsplash.com/photos/_FoHMYYlatI )

0개의 댓글