4. 타입 확장하기/좁히기

오민준·2024년 3월 6일
0

📘 타입 확장하기

타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는 것을 말한다.
기본적으로 interface와 type 키워드를 사용해 타입을 정의하고 extends, 교차 타입, 유니온 타입을 사용하여 타입을 확장한다.
여기서는 타입 확장의 장점과 extends, 교차 타입, 유니온 타입간의 차이를 파악하고자 한다.

💬 타입 확장의 장점

  • 기존에 작성한 타입을 바탕으로 타입 확장을 하므로 코드 중복을 줄일 수 있다.
  • 유니온 타입과 교차 타입을 사용한 새로운 타입은 오로지 type 키워드로만 선언할 수 있다.

💬 유니온 타입

type MyUnion = A | B
  • 유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다.
  • 집합의 관점에서 유니온 타입은 합집합이므로 A 타입과 B 타입의 모든 값이 MyUnion 타입의 값이 된다.
  • 유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근 가능하다.
interface CookingStep {
  orderId: string;
  price: number;
}

interface DeliveryStep {
  orderId: string;
  time: number;
  distance: string;
}

function getDeliveryDistance(step: CookingStep | DeliveryStep) {
  return step.distance;
  // Property ‘distance’ does not exist on type ‘CookingStep | DeliveryStep’
  // Property ‘distance’ does not exist on type ‘CookingStep’
}
  • getDeliveryDistance 함수는 CookingStep과 DeliveryStep의 유니온 타입 값을 step이라는 인자로 받는다.
  • 함수에서는 step.disatnce를 호출하지만 distance는 DeliveryStep에만 존재하므로 인자로 받는 step의 타입이 CookingStep일 때는 disatnce 속성을 찾을 수 없어 에러가 발생한다.
  • 즉, step이라는 유니온 타입은 CookingStep 혹은 DeliveryStep 타입에 해당할 뿐이지 CookingStep이면서 DeliveryStep인 것은 아니다.

💬 교차 타입

type MyIntersection = A & B
  • 마찬가지로 교차 타입 또한 기존 타입을 합쳐 하나의 타입을 만든다.
  • 그러나 유니온 타입과 달리 A 타입과 B 타입을 합쳐 모든 속성을 가진 단일 타입이 된다.
  • 집합의 관점에서 교차 타입은 교집합이므로 MyIntersection의 모든 원소는 집합 A의 원소이자 집합 B의 원소이다.
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});
console.log(배달 거리: ${progress.distance});
}

  • BaedalProgress 타입의 progress 값은 CookingStep이 가진 price 속성과 DeliveryStep이 가진 distance 속성을 모두 포함하고 있다.

💬 extends와 교차 타입

// extends 키워드를 사용해 교차 타입을 작성하였다.
interface B extends A {
  // 새로운 속성을 작성한다.
}
  • B는 A를 확장함으로써 A의 속성을 모두 포함하고 있다.
  • 따라서, B는 A의 모든 속성을 포함하는 사우이 집합이 되고 A는 B의 부분집합이 된다.
  • 이를 교차 타입의 관점에서 작성하면 아래와 같다.
// 유니온 타입과 교차 타입을 사용한 새로운 타입은 type 키워드로만 선언 가능하다.
type B = {
  // 새로운 속성을 작성한다.
} & A
  • 여기서 주의할 점은 extends 키워드를 사용한 타입은 교차 타입과 100% 상응하지는 않는다.
interface DeliveryTip {
  tip: number;
  }

interface Filter extends DeliveryTip {
  tip: string;
  // Interface ‘Filter’ incorrectly extends interface ‘DeliveryTip’
  // Types of property ‘tip’ are incompatible
  // Type ‘string’ is not assignable to type ‘number’
}

type DeliveryTip = {
  tip: number;
  };

type Filter = DeliveryTip & {
  tip: string;
};
  • DeliveryTip을 extends로 확장한 Filter 타입에 string타입의 속성 tip을 선언하면 tip의 타입이 호환되지 않는 에러가 발생한다.
  • 그러나 extends를 교차 타입으로 바꾸면 에러가 발생하지 않는다.
  • 이 때 tip 속성의 타입은 never로 교차 타입으로 선언될 때 새롭게 추가되는 속성을 미리 알 수 없으므로 선언시 에러가 발생하지는 않는다.
  • 그러나 tip이라는 같은 속성에 대해 서로 호환되지 않는 타입이 선언되어 never 타입이 된 것이다.

📗 타입 가드로 타입 좁히기

타입스크립트에서 타입 좁히기는 변수나 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정을 말한다.
타입 좁히기를 통해 더 정확하고 명시적인 타입 추론을 할 수 있게 되고, 복잡한 타입을 작은 범위로 축소하여 타입 안정성을 높일 수 있다.

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

  • 타입스크립트에의 분기 처리는 조건문과 타입 가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에 따라 다른 동작을 처리하는 것을 말한다.
  • 타입 가드는 런타임에 조건문을 사용하여 타입을 검사하고 타입 기능을 좁혀주는 기능을 말한다.
  • 컴파일 시에 타입 정보는 모두 제거되어 런타임에 존재하지 않으므로 if문 등을 사용해 로직을 처리할 수 없다
  • 따라서 특정 문맥 안에서 타입스크립트가 해당 변수를 원하는 타입으로 추론하도록 유도하면서도 런타임에서도 유효한 방법이 필요한데 이 때 타입 가드를 사용한다.
  • typeof, instanceof, in과 같은 자바스크립트 연산자를 사용하여 제어문으로 특정 타입 값을 가질수밖에 없도록 유도하여 자연스럽게 타입을 좁힌다.
  • 여기서 런타임에 유효하다는 말은 타입스크립트뿐만 아니라 자바스크립트에서도 사용 가능한 문법이어야 한다는 의미이다.

💬 typeof를 활용하여 원시 타입을 추론

  • typeof A === B를 조건으로 분기 처리하면 해당 분기 내에서는 A의 타입이 B로 추론된다.
  • 다만 typeof는 자바스크립트 타입 시스템만 대응 가능하므로 null이나 object 타입과 같은 복잡한 타입을 검증할 수는 없다.
  • typeof 연산자를 사용하여 검사할 수 있는 타입에는 string, number, boolean, undefined, object, function, bigint, symbol이 있다.
const replaceHyphen: (date: string | Date) => string | Date = (date) => {
  if (typeof date ===string) {
  // 이 분기에서는 date의 타입이 string으로 추론된다
  return date.replace(/-/g,/);
  }

  return date;
};

💬 instanceof를 활용하여 인스턴스화 된 객체 타입 판별

  • A instanceof B 형태로 사용하여 A는 타입을 검사할 대상 변수, B는 특정 객체의 생성자가 들어간다.
  • A의 프로토타입 체인에 생성자 B가 존재한다면 true를, 그렇지 않다면 false를 반환한다.

💬 in을 활용하여 객체의 속성 유무를 구분

  • A in B 형태로 사용하여 A라는 속성이 B에 존재하는 지를 검사한다.
  • 프로토타입 체인으로 접근할 수 있다면 전부 true를 반환한다.

💬 is로 사용자 정의 타입가드 만들기

  • A is B 형태로 사용하며 A는 매개변수 이름이고 B는 타입이다.
  • 참/거짓의 진릿값을 반환하면서 반환 타입을 타입 명제로 지정하게 되면 반환 값이 참 일때 A의 매개변수의 타입을 B 타입으로 취급한다.

📙 식별할 수 있는 유니온으로 타입 좁히기

종종 Tagged Union으로도 불리는 식별할 수 있는 유니온은 타입 좁히기에 널리 사용되는 방식이다.

💬 에러 정의하기

💬 식별할 수 있는 유니온

💬 식별할 수 있는 유니온의 판별자 선정

📔 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 {
    return “배민상품권”;
  }
};
  • 새로운 상품권이 생겨 ProductPrice 타입이 업데이트되면 getProductName 함수도 함께 업데이트 되어야 한다.
  • 이 때 productPrice가 "5000"일 경우의 조건도 검사하여 반환해야 한다.
  • 이와 같이 모든 타입에 대한 타입 검사를 강제하려면 아래와 같이 코드를 작성하면 된다.
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 타입 중 500에 대한 분기 처리를 하지 않아서 발생한 것이다.
  • 이렇게 모든 케이스에 대한 타입 분기 처리를 해주지 않았을 때, 컴파일타임 에러가 발생하게 하는 것을 Exhaustiveness Checking이라고 한다.
  • Exhaustiveness Checking 함수는 매개변수를 never 타입으로 선언하고 있다.
  • 이는 매개변수로 그 어떤 값도 받을 수 없으며 만일 값이 들어온다면 에러를 뱉는다.
  • 이 함수를 타입 처리 조건문의 마지막 else문에 사용한다면 앞의 조건문에서 모든 타입에 대한 분기 처리를 강제할 수 있다.
profile
ChatGPT-Driven Development를 지양합니다.

0개의 댓글