[우아한 타입스크립트] 4장 타입 확장하기, 좁히기

Jean Young Park·2024년 7월 1일
0

javascript

목록 보기
21/23

4.1 타입 확장하기


  • 타입 확장의 장점
    타입 확정의 가장 큰 장점은 코드 중복을 중링 수 있다는 것이다.
    interface BaseMenuItem {
    	itemName: string | null;
    	itemImageUrl: string| null;
    	itemDiscountAmout: number;
    	stock: number| null;
    }
    
    interface BaseCardItem extends BaseMenuItem {
    	quantity: number;
    }
    type키워드는 아래와 같이 작성할 수 있다.
    type BaseMenuItem = {
    	itemName: string | null;
    	itemImageUrl: string| null;
    	itemDiscountAmout: number;
    	stock: number| null;
    }
    
    type BaseCartItem = {
    	qantity: number;
    } & BaseMenuItem
  • 유니온 타입
    유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다.
    유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근할 수 있다.
    interface CookingStep {
    	orderId: string;
    	price: number;
    }
    
    interface DeliverStep {
    	orderId: string;
    	time: number;
    	distance: string;
    }
    
    function getDeliveryDistance(step: CookingStep | DeliverStep) {
    	return step.distance;
    	// Property 'distance' does not exist on type 'CookingStep | DeliverStep'
    	// Property 'distance' does not exist on type 'CookingStep
    }
  • 교차 타입
    기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 만드는 것으로 이해할 수 있다.
    interface CookingStep {
    	orderId: string;
    	time: number;
    	price: number;
    }
    
    interface DeliveryStep {
    	orderId: string;
    	time: number;
    	distance: string;
    }
    
    type BaedalProgress = CookingStep & DeliveryStep;
    
    function logBaedalInfo(progress: BaedalProgress) {
    	console.log(progress.price); // o
    }
    유니온은 합집합이라면 교차타입은 교집합의 개념과 비슷하다. 하지만 타입이 서로 호환되지 않는 경우도 있다.
    type IdType = string | number;
    type Numeric = number | boolean;
    
    type Universal = IdType & Numeric;
    이 경우에 4가지로 생각해 볼 수 있다.
    1. string이면서 numbe인 경우

    2. string이면서 boolean인 경우

    3. number이면서 number인 경우

    4. number이면서 boolean인 경우

      3번만 유효하기 떄문에 Universal의 값은 number가 된다.

  • extends와 교차타입
    extends를 사용해서 교차 타입을 작성할 수도 있다.
    type BaseMenuItem = {
    	itemName: string | null;
    	itemImageUrl: string| null;
    	itemDiscountAmout: number;
    	stock: number| null;
    }
    
    type BaseCartItem = {
    	qantity: number;
    } & BaseMenuItem
    교차 타입을 사용한 코드에서는 BaseMenuItem과 BaseCartItem를 interface가 아닌 type으로 선언했다. 왜냐하면 교차 아빙느 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있기 떄문이다.
    interface DeliveryTip {
    	tip: number;
    }
    
    interface Filter extends DeliveryTip {
    	tip: string;
    	// Interface 'Filter" incorrectly extends interface 'DeliveryTip'
    }
    DeliveryTip의 tip과 Filter의 tip이 호환되지 않는다는 에러가 발생한다.
     type DeliveryTip = {
    	tip: number;
    }
    
    type Filter = DeliveryTip & {
    	tip: string;
    }
    type으로 변경하고 &를 사용하면 에러가 발생하지 않는데 이때 타입은 never이 된다. type은 쿄차 타입은 로 선언되었을 떄 새롭게 추가되는 속성에 대해 미리 알 수 없기 때문에 선언 시 에러가 발생하지 않는다.

4.2 타입 좁히기 - 타입 가드


타입 좁히기는 변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정을 말한다.

  • 타입 가드에 따라 분기 처리하기
    타입스크립트에서의 분기 처리는 조건문과 타입 가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에 따라 다른 동작을 수행하는 것을 말한다.

    💡 타입 가드
    타입 가드는 런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능을 말한다.
    💡 스코프
    변수와 함수 등의 식별자가 유효한 범위를 나타낸다. 즉, 변수와 함수를 선언하거나 사용할 수 있는 영역을 말한다.

    타입스크립트로 개발하다 보면 여러 타입을 할당할 수 있는 스코프에서 특정 타입을 조건으로 만들어 분기 처리하고 싶을 때가 있다.
    id문을 사용해서 처리하면 될 것 같지만 컴파일 시 타입 정보는 모두 제거되어 런타임에 존재하지 않게 된다. 이 때 사용할 수 있는게 타입 가드이다.
    타입 가드는 크게 자바스크립트 연산자를 사용한 타입 가드와 사용자 정의 타입 가드로 구분된다.
    자바스크립트 연산자를 활용한 타입 가드는 typeof, instanceof, in과 같은 연산자를 사용한다. 자바스크립트 연산자를 사용하는 이유는 런타입에 유효한 타입 가드를 만들기 위해서다. 런타임에서 유효하다는 말은 타입스크립트뿐만 아니라 자바스크립트에서도 사용할 수 있는 문법이어야 한다는 의미다.
  • 원시 타입을 추론할 때 : typeof 연산자 활용하기
    typeof 연산자는 주로 원시 타입을 좁히는 용도로만 사용할 것을 권장한다.
  • 인스턴스화된 객체 타입을 판별할 때 : instanceof 연산자 활용하기
    typeof는 주로 원시 타입을 판별하는 데 사용한다면, instanceof 연산자는 인스턴트화된 객체 타입을 판별하는 타입 가드로 사용할 수 있다. A instanceof B 형태로 사용하며 A 프로토타입 체인에 생성자 B가 존재하는지를 검사한다.
  • 객체의 속성이 있는지 없는지에 따는 구분: in 연산자 활용하기
    in연산자는 A in B의 형태로 사용하는데 이름 그대로 A라는 속성이 B 객체에 존재하는지를 검사한다.
  • is 연산자로 사용자 정의 타입 가드 만들어 활용하기
    직접 타입 가드 함수를 만들 수도 있다. 이러한 방식의 타입 가드는 반환 타입이 타입 명제인 함수를 정의하여 사용할 수 있다. 타입 명제는 A is B 형식으로 작성하면 되는데 A는 매개변수 B는 타입이다.
    const isDestinationCode = (x: string): x is DestinationCode => destinationCodeList.includes(x);
    isDestinationCode는 string 타입의 매개변수가 destinationCodeList 배열의 원소 중 하나인지를 검사하여 boolean을 반환하는 함수이다.

4.3 타입 좁히기 - 식별할 수 있는 유니온


  • 에러 정의하기
    각 에러 타입을 다음과 같이 정의했다고 해보자
    type TextError = {
    	errorCode: string;
    	errorMessage: string;
    }
    type ToastError = {
    	errorCode: string;
    	errorMessage: string;
    	toastMessage: string;
    	toastShowDuration: number; // 토스트를 띄워줄 시간
    }
    type AlerError = {
    	errorCode: string;
    	errorMessage: string;
    	onConfirm: () => void;
    }
    이 에러 타입의 유니온 타입을 원소로 하는 배열을 정의해보면 다음과 같다.
    type ErrorFeedbackType = TextError | ToastError | AlertError;
    const errorArr: ErrorFeedbackType[] = [
    	{ errorCode: "100", errorMessage: "텍스트 에러" },
    	{ errorCode: "100", errorMessage: "텍스트 에러", toastShowDutaion: 3000 },
    	{ errorCode: "300", errorMessage: "얼럿 에러", onConfirm: () => {} }
    ];
    errorArr는 다양한 에러 객체를 관리할 수 있게 되었다.
    const errorArr: ErrorFeedbackType[] = [
    	//...
    	{
    		errorCode: "999",
    		errorMessage: "잘못된 에러",
    		toastShowDuration: 3000,
    		onConfirm: () => {}
    	}
    ]
    하지만 이 코드를 작성했을 때 자바스크립트는 덕 타이핑 언어이기 때문에 별도의 타입 에러를 뱉지 않는 것을 확인할 수 있다.
  • 식별할 수 있는 유니온
    따라서 에러 타입을 구분할 방법이 필요하다. 식별할 수 있는 유니온이랑 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 판별자를 달아주어 포함 관계를 제거하는 것이다. 판별자의 개념으로 errorType이라는 필드를 새로 정의해보자
    type TextError = {
    	errorType: "TEXT";
    	errorCode: string;
    	errorMessage: string;
    }
    type ToastError = {
    	errorType: "TOAST";
    	errorCode: string;
    	errorMessage: string;
    	toastShowDuration: number;
    }
    type AlertError = {
    	errorType = "ALERT";
    	errorCode: string;
    	errorMessage: string;
    	onConfirm: () => void;
    }
    에러 객체에 대한 타입을 위와 같이 정의한 상태에서 errorArr을 새로 정의해보자
    type ErrorFeedbackType = TextError | ToastError | AlertError;
    const errorArr: ErrorFeedbackType[] = [
    	{ errorType: "TEXT", errorCode: "100", errorMessage: "텍스트 에러" },
    	{
    		errorType: "TOAST",
    		errorCode: "200",
    		errorMessage: "토스트 에러",
    		toastShowDuration: 3000
    	},
    	{
    		errorType: "TEXT",
    		errorCode: "999",
    		errorMessage: "잘못된 에러",
    		toastShowDuration: 3000 // Object literal may only specify known properties, and 'toastShowDutaion' does not exist in type 'TextError'
    	}
    ]
    정확하지 않은 에러 객체에 대해 타입 에러가 발생하는 것을 확인할 수 있다.
  • 식별할 수 있는 유니온의 판별자 선정
    식별할 수 있는 유니온의 판별자는 유닛 타입으로 선언되어야 정삭적으로 동작한다.
    유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다.

4.4 Exhaustiveness Checking으로 정확한 타입 분기 유지하기


모든 케이스에 대해 철저하게 타입을 검사하는 것을 말한다.

type ProductPrice = "10000" | "20000";

const getProductName = (productPrice: ProductPrice): string => {
	if(productPrice === "10000") return "배민상품권 1만 원";
	if(productPrice === "20000") return "배민상품권 2만 원";
	else {
		return "배민상품권";
	}
}

여기서 ProductPrice 타입이 업데이트 되어야 한다고 해보자.

type ProductPrice = "10000" | "20000" | "50000";

const getProductName = (productPrice: ProductPrice): string => {
	if(productPrice === "10000") return "배민상품권 1만 원";
	if(productPrice === "20000") return "배민상품권 2만 원";
	if(productPrice === "50000") return "배민상품권 5만 원";
	else {
		return "배민상품권";
	}
}

위와 타입 업데이트와 같이 조건도 검사하여 의도한 대로 상품권 이름을 반환해야 한다. 그러나 getProductName 함수를 수정하지 않아도 별도 에러가 발생하는 것이 아니기 때문에 실수할 여지가 있다. 이와 같이 모든 타입에 대한 타입 검사를 강제하고 싶다면 아래와 같이 코드를 작성할 수 있다.

type ProductPrice = "10000" | "20000" | "50000";

const getProductName = (productPrice: ProductPrice): string => {
	if(productPrice === "10000") return "배민상품권 1만 원";
	if(productPrice === "20000") return "배민상품권 2만 원";
	else {
		exhaustiveCheck(productPrice);
		return "배민상품권";
	}
}

const exhaustiveCheck = (param: never) => {
	throw new Error("type error!");
}

0개의 댓글

관련 채용 정보