개발의변화
우아한타입스크립트 4장 본문
타입 확장이란?
기존 타입을 사용해서 새로운 타입을 정의
타입스크립트 타입 지정 동작원리
- 타입 정의
ex)interface, type
- 타입 확장
ex)extends, 교차타입, 유니온타입
그러면 타입 확장의 장점은?
코드 중복 줄이기!
코드를 작성할 때 불필요한 중복 타입을 선언하기 보단 기존에 작성한 타입 바탕으로 확장
EX)Interface로 지정할 때
interface BaseMenuItem {
itemName: string | null;
itemImageUrl: string | null;
}
interface BaseCartItem extends BaseMenuItem {
quantity: number;
}
BaseCartItem에 BaseMenuItem 확장하여 사용
EX) type으로 지정할 때
type BaseMenuItem = {
itemName: string | null;
itemImageUrl: string | null;
}
type BaseCartItem = {
quantity: number;
} & BaseMenuItem;
유니온 타입
2개 이상의 타입을 조합하여 사용하는 방법 - 합집합개념
Point) 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 오직 접근가능
interface CookingStep {
orderId: string;
price: number;
}
interface DeliveryStep {
orderId: string;
time: number;
}
function checkTime(step: CookingStep | DeliveryStep) {
return step.distance //Error
}
교차타입
기존타입 합쳐 필요한 모든 기능을 가진 하나의 타입
interface CookingStep {
orderId: string;
price: number;
}
interface DeliveryStep {
orderId: string;
time: number;
}
function checkTime(step: CookingStep & DeliveryStep) {
return step.distance
}
에러가 안뜨는 이유: 교차 타입은 두 타입의 교집합을 의미 즉 DeliveryTip과 StarRating의 속성을 모두 포함한 타입
하지만 교차 타입을 사용할 때 서로 호환되지 않는 경우동 있다
type IdType = string|number;
type Numeric = number | boolean
type Universal = IdType & Numberic
Universal은 IdType과 Numberic의 교차타입이므로 두 타입 모두 만족하는 경우에만 유지 number!
extends와 교차타입
interface BaseMenuItem{
itemName: string | null;
itemImageUrl : string | null;
itemDiscountAMount : number;
}
interface BaseCartItem extends BaseMenuItem {
quantity: number;
}
BaseCartItem은 BaseMenuItem을 확장함으로써 BaseMenuItem의 속성을 모두 포함.
하지만 유니온과 교차타입은 type애서만 사용가능함
type BaseMenuItem = {
itemName: string | null;
itemImageUrl: string | null;
}
type BaseCartItem = {
quantity: number ;
} & BaseMenuItem;
교차 타입과 비슷해 보일 수 있찌만 extends는 100%상응하지 않는다는 것
interface DeliveryTip {
tip: number;
}
interface Filter extends DeliveryTip{
tip: string;
}
배달의민족 메뉴 시스템에 타입 확장 적용하기
interface Menu {
name: string;
image: string;
}
function MainMenu(){
const menuList: Menu[] = [{name: "1인분", image: "1인분.png"}]
return(
<ul>
{
manuList.map((menu) => (
<li>
<img src = {menu.image} />
<span> {menu.name} </span>
</li>
))
}
</ul>
)
}
요구사항
1.특정메뉴를 길게 누르면 gif파일 재생
2.특정 메뉴는 이미지 대신 별도의 텍스트만 노출
1.하나의 타입에 여러 속성을 추가할 떄
interface Menu {
name: string;
image: string;
gif?: string;
text?: string;
}
menu 인터페이스 하나에 모든걸 설정했을 때
specialMenuList.map((menu) => menu.text);
//TypeError: Cannot read properties of undefined
specialMenuList는 Menu 타입의 원소를 가지지만 text 속성에도 접근가능하지만 모든 원소가 text를 안가지기에 에러 발생
2.extends를 활용해 나타낼 떄
interface SpecialMenu extends Menu {
gif: string;
}
specialMenuList.map((menu) => menu.text); // property 'text' does not exist on type
프로그램을 실행하지 않고도 타입이 잘못되었다는 걸 바로 확인가능
타입좁히기 - 타입가드
변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정
타입 좁히기 -> 더 정확하고 명시적인 타입 추론을 할 수 있겟되고 복잡한 타입을 작은 범위로 축소하여 타입 안정성 높임
타입 가드: 런타임에 조건물을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능
EX)
A | B의 매개변수를 어떤 함수가 받을 때 인자 타입을 A와 B를 구분해서 로직을 처리하고 싶을 때
[X]: if문으로는 컴파일시 타입 정보가 사라져 조건을 만들 수 없다
결국, 특정 문맥 안에서 타입스크립트가 해당 변수를 타입 A로 추론하면서 유도하면서 런타임에서도 유효한 방법인 타입 카드를 사용해야 한다.
방식1: 자바스크립트 연산자를 활용한 타입카드 (typeof, instanceof,in 연산자 활용)
1) typeof - 원시 타입 추론
원시형: string, number, boolean, undefined, object, function, bigint, symbol
typeof A === B 조건으로 분기처리하여 A의 타입을 B로 추론
한계 null, 배열,object 타입을 확인할 수 없다 -> typeof는 자바스크립트 타입 시스템이기 때문
const replaceHyphen: (date: string | Date) => string | Date = (date) => {
if (typeof date === “string”) {
// 이 분기에서는 date의 타입이 string으로 추론된다
return date.replace(/-/g, “/”);
}
return date;
};
2) instanceof - 인스턴스화된 객체 타입 판별
interface Range {
start: Date;
end : Date;
}
interface DatePickerProps {
selectedDates?: Date: Range;
}
const DatePicker = ({ selectedDates }: DatePickerProps) => {
const [selected,setSelected] = useState(convertToRange(selectedDates));
}
export function converToRange(selected?: Date | Range): Range| undefiend {
return selected instanceof Date
? { start: selected, end: selected}
: selected;
}
타입을 검사할 대상 변수 instanceof 특정객체 생성자
A의 프로토타입 체인에 생성자 B가 존재하는지를 검사하여 존재하면 true, 아니면 false
3) in 연산자 - 객체의 속성이 있는지 없는지 구분
interface BasicNoticeDialogProps {
noticeTitle: string;
noticeBody: string;
}
interface NoticeDialogWithCookieProps extends BasicNoticeDialogProps {
cookieKey: string;
noForADay?: boolean;
neverAgain?: boolean
}
const NoticeDialog: React.FC<NoticeDialogProps> = (props) => {
if ("cookieKey" in props) return <NoticeDialogWithCookie {...props} />
return <NoticeDialogBase {...props}/>
}
자바스크립트의 in 연산자는 런타임의 값만을 검사하지만 타입스크립트에서 객체 타입에 속성이 존재하는지 검사
방식2: 사용자 정의 타입 가드는 사용자가 직접 어떤 타입으로 값을 좁힐지 지정하는 방법
4) is 연산자 - 사용자 정의 타입 가드
'매개변수is타입' 형식으로 타입명제를 작성하면 된다.
const isDestinationCode = (x: string): x is DestinationCode => destinationCodeList.includes(x);
함수의 반환 값을 boolean이 아닌 x is D
그러면 boolean을 쓰면 안되나???
const getAvailableDestinationNameList = async (): Promise<DestinationName[]> => {
const data = await AxiosRequest<string[]>("ㅎㄷㅅ",".../destinations");
const destinationNames : DestinationName[] = [];
data?.forEach((str) => {
if (isDestinationCode(str)) {
destinationNames.push(DestinationNameSet[str]);
}
});
return destinationNames;
}
isDestinationcode의 x is DestinationCode가 boolean이면 -> 타입 추론 불가능(불린이지만 Destination것인지느 모르기 떄문에)
타입 좁히기 - 식별할 수 있는 유니온(Discriminated Unions)
Ex) 배달의 민족 선물하기 서비스는 선물을 보낼 떄 필요한 값을 사용자가 올바르게 입력햇는지를 확인하는 유효성 검사 진행
유효성에러 = 에러코드 + 에러메시지, 에러 노출 방식에 따라 추가로 필요한 정보 있을 수 있음
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: “100”, errorMessage: “텍스트 에러” },
{ errorCode: “200”, errorMessage: “토스트 에러”, toastShowDuration: 3000 },
{ errorCode: “300”, errorMessage: “얼럿 에러”, onConfirm: () => {} },
];
근데 이제 ToastError의 toastShowDuration 필드와 AlertError의 onConfirm 필드를 가지는 객체에 대해 타입에러를 뱉을까?
const errorArr: ErrorFeedbackType[] = [
// ...
{
errorCode: “999”,
errorMessage: “잘못된 에러”,
toastShowDuration: 3000,
onConfirm: () => {},
},
];
자바스크립트는 덕 타이핑이라 별도의 타입에러를 뱉지 않는다.
식별할 수 있는 유니온
타입들이 서로 포함 관계를 가지지 않도록 정의 , 식별할 수 있는 유니온이란 타입 간의 구조 호환을 막기 위해 판별자를 달아줘 제거
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;
};
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’
},
];
식별할 수 있는 유니온을 통해 에러 객체에 대한 타입 에러 발생확인 가능
식별할 수 있는 유니온의 판별자 선정
식별할 수 있는 유니온의 판별자는 유닛타입으로 선언되어야 정상적 동작
유닛타입: 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값 (null,undefined,리터럴, ture, 1 등 정확한 값 / void,string,number은 포함x)
interface A {
value: “a”; // unit type
answer: 1;
}
interface B {
value: string; // not unit type
answer: 2;
}
interface C {
value: Error; // instantiable type
answer: 3;
}
type Unions = A | B | C;
function handle(param: Unions) {
/** 판별자가 value일 때 */
param.answer; // 1 | 2 | 3
// ‘a’가 리터럴 타입이므로 타입이 좁혀진다.
// 단, 이는 string 타입에 포함되므로 param은 A 또는 B 타입으로 좁혀진다
if (param.value === “a”) {
param.answer; // 1 | 2 return;
}
// 유닛 타입이 아니거나 인스턴스화할 수 있는 타입일 경우 타입이 좁혀지지 않는다
if (typeof param.value === “string”) {
param.answer; // 1 | 2 | 3 return;
}
if (param.value instanceof Error) {
param.answer; // 1 | 2 | 3 return;
}
/** 판별자가 answer일 때 */
param.value; // string | Error
// 판별자가 유닛 타입이므로 타입이 좁혀진다
if (param.answer === 1) {
param.value; // ‘a’
}
}
Exhaustiveness Checking - 정확한 타입 분기
Exhaustiveness: 철저함, 완전함
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” | “5000”;
const getProductName = (productPrice: ProductPrice): string => {
if (productPrice === “10000”) return “배민상품권 1만 원”;
if (productPrice === “20000”) return “배민상품권 2만 원”;
if (productPrice === “5000”) return “배민상품권 5천 원”; // 조건 추가 필요
else {
return “배민상품권”;
}
};
이렇게 수정하면 괜찮으나 실수로 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 {
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!”);
};
exhaustiveCheck(productPrice)에서 에러를 뱉고 있는데 ProductPrice타입 중 5000이라는 값에 대한 분기 처리가 되지 않았기 때문이다.
이렇게 모든 케이스에 타입 분기 처리를 하기 위해선 exhaustiveCheck를 활용하면 되고 매개변수를 never타입을 띄우는데 never는 매개변수 어떤 값도 받을 수 없으면서 만일 값이 들어오면에러를 내뱉는다.