Search
🛡️

다양한 타입 좁히기 테크닉 (타입 가드)

변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정
더 정확하고 명시적인 타입 추론 가능
복잡한 타입을 작은 범위로 축소하여 타입 안정성을 높힘
타입 좁히기의 5가지 방법
타입가드
typeof
instanceof
in
is

1. 타입 가드에 따라 분기 처리하기

런타임에 조건문을 사용해 타입 검사 → 타입 범위 좁혀주는 기능
예시
여러 타입 할당 가능한 스코프에서 특정 타입에 대한 분기 처리
컴파일 시 타입 정보는 제거 되기에, 컴파일해도 사라지지 않는 방법 필요

1-1. 자바스크립트 연산자를 활용한 타입가드

제어문을 통해 특정 값을 가질 수 밖에 없는 상황 유도
런타임에 유효한 타입 가드를 만들 때 유용함

1-1-1. 원시 타입을 추론할 때: typeof 연산자 활용하기

typeof 연산자는 값의 타입 추론 가능
복잡한 타입 검증에는 한계 (null, 배열 등이 object로 판별하는 등)
그래서 주로 원시 타입을 좁히는데에 유용

1-1-2. 인스턴스화된 객체 타입을 판별할 때: instanceof 연산자 활용하기

A instanceof B // A에는 타입을 검사할 대상 변수, B에는 특정 객체의 생성자 (B가 상위 개념) // A의 프로토타입 체인에 생성자 B가 존재하면 true // A의 프로토타입 체인에 생성자 B가 존재하지 않으면 false
TypeScript
복사
프로토타입 속성 변화에 따라 instanceof 연산자의 결과가 달라질 수 있음

1-1-3. 객체의 속성이 있는지 없는지에 따른 구분: in 연산자 활용하기

A in B // A 라는 속성이 B 객체에 존재하는지 검사
TypeScript
복사
속성의 값이 falsy해도 속성 자체가 존재한다면 결과값은 true가 됨

1-2. is 연산자로 사용자 정의 타입 가드 만들어 활용하기

반환 타입이 타입 명제(type predicates)인 함수를 정의해서 사용
const isDestinationCode = (x: string): boolean => destinationCodeList.includes(x); const getAvailableDestinationNameList = async (): Promise<DestinationName[]> => { const data = await AxiosRequest<string[]>(get,.../destinations”); const destinationNames: DestinationName[] = []; data?.forEach((str) => { // isDestinationCode의 반환값이 // 1. boolean일 경우 -> str이 단순 string으로 추론 됨 // 2. is 연산자일 경우 -> str이 destinationCodeList의 값 중 하나로 추론 됨 if (isDestinationCode(str)) { // str이 destinationCodeList의 원소가 맞는지 체크. destinationNames.push(DestinationNameSet[str]); // 맞다면 DestinationNames 배열에 push. /* isDestinationCode의 반환 값에 is를 사용하지 않고 boolean이라고 한다면 다음 에러가 발생한다 - Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘Record<”MESSAGE_PLATFORM” | “COUPON_PLATFORM” | “BRAZE”, // string[] 타입인 str을 DestinationName[]에 push할 수 없다는 에러 “통합메시지플랫폼” | “쿠폰대장간” | “braze”>’ */ } }); return destinationNames; };
TypeScript
복사

1-3. 식별할 수 있는 유니온

비슷하지만 다른 타입이 호환되지 않도록 판별자가 되는 속성을 추가
태그된 유니온(tagged union) 또는 식별할 수 있는 유니온(discriminated union)으로 불림
예시
type TextError = { errorCode: string; errorMessage: string; }; type ToastError = { errorCode: string; errorMessage: string; toastShowDuration: number; // 토스트를 띄워줄 시간 }; type AlertError = { errorCode: string; errorMessage: string; onConfirm: () => void; // 얼럿 창의 확인 버튼을 누른 뒤 액션 }; type ErrorFeedbackType = TextError | ToastError | AlertError; const errorArr: ErrorFeedbackType[] = [ { errorCode: "999", errorMessage: "잘못된 에러", // 실제로 아래 두 개의 추가 속성을 가지는 타입을 없으나 덕타이핑으로 인해 TS에서는 가능 toastShowDuration: 3000, onConfirm: () => {}, }, ];
TypeScript
복사
해결책
type ErrorFeedbackType = TextError | ToastError | AlertError; const errorArr: ErrorFeedbackType[] = [ { errorType:TEXT, errorCode:100, errorMessage: “텍스트 에러” }, { errorType:TOAST, errorCode:200, errorMessage: “토스트 에러”, toastShowDuration: 3000, }, { errorType:ALERT, errorCode:300, errorMessage: “얼럿 에러”, onConfirm: () => {}, }, { errorType:TEXT, errorCode:999, errorMessage: “잘못된 에러”, toastShowDuration: 3000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘TextError’ onConfirm: () => {}, }, { errorType:TOAST, errorCode:210, errorMessage: “토스트 에러”, onConfirm: () => {}, // Object literal may only specify known properties, and ‘onConfirm’ does not exist in type ‘ToastError’ }, { errorType:ALERT, errorCode:310, errorMessage: “얼럿 에러”, toastShowDuration: 5000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘AlertError’ }, ];
TypeScript
복사
식별할 수 있는 유니온의 판별자 선정
반드시 판별자가 유닛타입으로 선언되어야 정상동작
유닛타입 - 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입
O - ull, undefined, 리터럴 타입을 비롯해 true, 1
X - void, string, number 등

1-3. Exhaustiveness Checking 으로 정확한 타입 분기 유지

일반적으로는 필요한 케이스에 대한 타입 분기 처리를 하면 요구 사항 충족
그러나, 모든 케이스에 대해 분기 처리를 해야 유지보수 시 안전하다 느끼는 경우
예시
상품권 가격별 이름을 반환하는 로직
type ProductPrice =10000|20000; const getProductName = (productPrice: ProductPrice): string => { if (productPrice ===10000) return “배민상품권 1만 원”; if (productPrice ===20000) return “배민상품권 2만 원”; else { return “배민상품권”; } };
TypeScript
복사
케이스가 추가되면 ProductPrice과 getProductName를 동시 수정해야하나
휴먼에러로 인해 진행되지 않으면 에러의 가능성
type ProductPrice =10000|20000|5000; const getProductName = (productPrice: ProductPrice): string => { if (productPrice ===10000) return “배민상품권 1만 원”; if (productPrice ===20000) return “배민상품권 2만 원”; if (productPrice ===5000) return “배민상품권 5천 원”; // 조건 추가 필요 else { return “배민상품권”; } };
TypeScript
복사
Exhaustiveness Checking 이용 시 안전하게 컴파일 타임에 확인 가능
type ProductPrice = "10000" | "20000" | "5000"; const getProductName = (productPrice: ProductPrice): string => { if (productPrice === "10000") return "배민상품권 1만 원"; if (productPrice === "20000") return "배민상품권 2만 원"; if (productPrice === "5000") return "배민상품권 5천 원"; else { exhaustiveCheck(productPrice); // productPrice의 type은 never로 추론 됨 return "배민상품권"; } }; const exhaustiveCheck = (param: never) => { throw new Error("type error!"); };
TypeScript
복사
type ProductPrice =10000|20000|5000; const getProductName = (productPrice: ProductPrice): string => { if (productPrice ===10000) return “배민상품권 1만 원”; if (productPrice ===20000) return “배민상품권 2만 원”; // if (productPrice === “5000”) return “배민상품권 5천 원”; else { exhaustiveCheck(productPrice); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘never’ return “배민상품권”; } }; const exhaustiveCheck = (param: never) => { throw new Error(type error!); };
TypeScript
복사