본문으로 건너뛰기

"extends" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

모든 태그 보기

· 약 12분
고현수

타입 확장의 장점과 더불어 extends, 교차 타입, 유니온 타입간의 차이를 파악하고 언제 사용하면 좋을지 살펴보자.
본문 p.120

타입 확장하기

타입 확장의 장점

  • 코드 중복을 줄일 수 있다.

    • DRY(Don't Repeat Yourself)는 타입에서도 적용된다.
  • 명시적인 코드를 작성할 수 있다.(어디에서 확장 되었다는 것을 쉽게 확인 가능하다.)

  • 코드 작성 단계에서 예기치 못한 버그도 예방 가능하다.

  • 예시

    • interface에서 확장

      interface Table {
      id: number;
      name: string;
      coordinate: string;
      dimension: string;
      }

      interface ModifyTable extends Table {
      color?: string;
      }
    • type을 활용한 확장

      type Table = {
      id: number;
      name: string;
      coordinate: string;
      dimension: string;
      };

      type ModifyTable = {
      color?: string;
      } & Table;
  • 타입 확장은 확장성이라는 장점을 가진다.

    • 여러 타입으로 확장 가능

      interface Table {
      id: number;
      name: string;
      coordinate: string;
      dimension: string;
      }

      interface ModifyTable extends Table {
      color?: string;
      }

      interface CheckConnectTable extends Table {
      connect?: boolean;
      occupy?: boolean;
      }

유니온 타입 (Union Type)

유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다. 집합의 관점으로 보면 유니온 타입을 합집합으로 해석할 수 있다.
p.122

type Union = A | B;

유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고있는 속성에만 접근할 수 있다.

예제

interface Human {
name: string;
age: number;
}

interface Humanoid {
id: string;
createdAt: string;
}

function getAge(object: Human | Humanoid) {
return object.age;
// Property 'age' does not exist on type 'Human | Humanoid'.
// Property 'age' does not exist on type 'Humanoid'.
}

교차 타입(Intersection Type)

교차 타입은 교집합의 개념과 비슷하다.
p.124

type Intersection = A & B;

예제

interface Human {
name: string;
age: number;
}

interface Humanoid {
id: string;
createdAt: string;
}

type NewWorldPeople = Human & Humanoid;

function getAge(object: NewWorldPeople) {
return object.age;
}

💡궁금하다 궁금해!

책의 본문에는 유니온 타입과 교차 타입에 대해서 이렇게 설명하고 있다.

타입스크립트의 타입을 속성의 집합이 아니라 값의 집합이라고 생각해야 유니온 타입이 합집합이라는 개념을 이해할 수 있다.
p.123

책에서는 속성이 아닌 값의 집합이라고 생각해야한다고 강조한다.(두 번) 하지만 어쨌든 직관적으로 동작하고 있는 모습은 Union 타입은 교집합처럼 동작하고 Intersection 타입은 합집합 처럼 느껴진다. 위에서 들었던 예제들을 보면 합집합이라니까 당연스레 object에 age가 있을 것으로 생각된다. 하지만 그렇지 않다. 도대체 왜이러는 걸까?

stackOverflow에 나와 똑같은 생각을 해서 질문을 올린 사람이 있는데 정말 여러 답변들이 달렸고 내가 제일 도움이 됐던 답변의 링크를 첨부한다.

이 분의 답변은 책에 적혀있는 속성의 집합이 아닌 값의 집합이라고 생각해야한다는 것의 의미를 잘 설명해주고 있다. 위의 Human과 Humanoid를 속성이 아닌 값의 관점에서 Union과 Intersection을 설명할 수 있다.

  • Union : Human의 값도 되고 Humanoid의 값도 된다. 그리고 Human과 Humanoid가 합쳐진 NewWorldPeople의 값도 된다.
  • Intersection : 오직 NewWorldPeople만 된다.

답변의 예시중 하나를 인용하고자한다.

type A = {
x: number;
y: number;
};

type B = {
y: number;
z: number;
};

type I = A & B;
type U = A | B;

let i: I;
let u: U;

i = { x: 1, y: 2 }; // <- error
i = { y: 2, z: 3 }; // <- error
i = { x: 1, y: 2, z: 3 };

u = { x: 1, y: 2 };
u = { y: 2, z: 3 };
u = { x: 1, y: 2, z: 3 };

타입 좁히기 - 타입가드

타입 가드는 크게 자바스크립트 연산자를 사용한 타입 가드와 사용자 정의 타입 가드로 구분할 수 있다.

  1. 원시 타입을 추론할 때 : typeof 연산자 활용하기
  2. 인스턴스화된 객체 타입을 판별할 때 : instanceof 연산자 활용하기
  3. 객체의 속성이 있는지 없는지에 따른 구분 : in 연산자 활용하기
  4. is 연산자로 사용자 정의 타입 가드 만들어 활용하기

자세한 내용은 책을 참조하기.

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

종종 태그된 유니온으로도 불리우는 식별할 수 있는 유니온은 타입 좁히기에 널리 사용되는 방식이다.
p.139

이 예시는 본문의 예시를 그대로 인용한다. 왜냐하면 우리 코드에도 이와 같은 에러 처리 방식이 똑같이 적용 될 수 있기 때문이다.

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: () => {} },
{
errorCode: "999",
errorMessage: "잘못된 에러",
onConfirm: () => {},
toastShowDuration: 3000
} // expect error
];

타입을 이렇게 거창하게 작성한 이유는 아마도 errorCode 999를 가지는 저 객체에 대해서 에러를 기대했기 때문일 것이다. 그러나 이런 상황에서 자바스크립트는 덕 타이핑 언어이기 때문에 별도의 타입 에러를 뱉지 않는다.

식별할 수 있는 유니온

각 타입이 비슷한 구조를 가지지만 서로 호환되지 않도록 만들어 주기 위해서는 서로 포함 관계를 가지지 않도록 정의해야한다.
식별할 수 있는 유니온이란 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 판별자를 달아주어 관계를 제거하는 것이다.

예제

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에 errorType이 포함되어 자바스크립트가 errorCode 999를 가진 객체에 대해 에러를 반환한다.

  // (생략)
{
type:"TEXT",
errorCode: "999",
errorMessage: "잘못된 에러",
onConfirm: () => {},
toastShowDuration: 3000
}
];

// Object literal may only specify known properties, and 'onConfirm' does not exist in type 'TextError'.

판별자 선정

  • 리터럴 타입이어야한다.
  • 인스턴스화할 수 있는 타입은 포함되지 않아야한다.

Exhaustiveness Checking

never 활용에 흔치 않은 예제라 정리해봤다.

모든 타입에 대한 타입 검사를 강제하고 싶다면 다음과 같이 코드를 작성하면 된다.
p.146-147

예제

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);
// Argument of type 'string' is not assignable to parameter of type 'never'.
// 'param' is declared but its value is never read.
return "배민상품권";
}
};

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

마무리

타입을 집합으로써의 개념으로 설명하는 것이 사실 이해가 잘 안됐었다. 하지만 '속성'이 아닌 '값'이라는 개념과 그것을 실마리 삼아 나름대로 이해를 할 수 있었다. 합집합과 교집합은 값으로써의 합집합과 교집합의 개념이다. 첨부한 stackoverflow의 링크에 자기 스스로 이해가 잘 되는 답변을 채택해서 다른 사람에게 설명할 수 있으면 될 것 같다.

타입 좁히기는 instanceof나 if문으로 값이 있는지 없는지 체크해서 막는 것이었는데 typeof나 in, is와 같은 자바스크립트 연산자를 활용한 타입 가드가 있다는 사실을 새롭게 배우게 되었다. 식별할 수 있는 유니온은 '이펙티브 타입스크립트'에서 본 개념이지만 실무에선 활용하지 못했었습니다. 그러나 Error 예제를 보고 나니 회사의 코드에 적용해 볼 수 있을 것 같다는 생각이 들었다.

마지막으로 never는 별로 실용적이거나 활용할 일이 없다고 생각했는데 챕터 마지막에 소개된 exhaustiveCheck를 never를 활용하기도 하는구나 하는 생각이 들었다. 예전에 Toast UI blog에서 타입스크립트의 Never 타입 완벽 가이드라는 글을 읽었던 적이 있는데, 사실 이게 뭐지? 라는 생각이 들었지만 이번에 실용적인 쓰임세를 알게되었다. 책을 다 읽었을 때 실무적으로 조금 더 완전하게 타입스크립트를 사용할 수 있을 것 같은 기대감이 생겼다.

· 약 4분
성은지

extends와 제너릭을 활용한 조건부 타입

🔍 내용 (151쪽)

  • PayMethod 타입은 제너릭 타입으로 extends를 사용한 조건부 타입이다.
type PayMethod<T> = T extends "card" ? Card : Bank;

🎉 이렇게 개선하면 어떨까요?

// 전
export type PointInfo = {
saleAmount: number;
saleProducts: string;
saleType: SaleType;
savePoint: number;
saveStamp: number;
type: string;
usePoint: number;
useStamp: number;
visitedAt: string;
};

// 후
type SaleType = "point" | "stamp";

type BasePointInfo<T extends SaleType> = {
saleAmount: number;
saleProducts: string;
saleType: T;
visitedAt: string;
};

type PointInfoExtended<T extends SaleType> = BasePointInfo<T> & {
type: T;
savePoint: T extends "point" ? number : 0;
usePoint: T extends "point" ? number : 0;
saveStamp: T extends "stamp" ? number : 0;
useStamp: T extends "stamp" ? number : 0;
};

// point 타입의 예시
const pointInfo: PointInfoExtended<"point"> = {
saleAmount: 100,
saleProducts: "Product A",
saleType: "point",
savePoint: 10,
usePoint: 20,
saveStamp: 0,
useStamp: 0,
type: "point",
visitedAt: "2024-03-14",
};

infer를 활용해서 타입 추론하기

🔍 내용 (156쪽)

  • extends를 사용할 때 infer 키워드를 사용할 수 있다. infer는 '추론하다'라는 의미를 지니고 있는데 타입스크립트에서도 단어 의미처럼 타입을 추론하는 역할을 한다.

🎉 예시

type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;

함수 리턴 타입이 있어? 있으면 그거, 없으면 any

🔍 내용 (159쪽)

  • menuList에서 subMenus가 없는 MainMenunamesubMenus에서 쓰이는 name, route name에 동일한 문자열만 입력해야 한다는 제약이 존재한다.
    • 코드로 작성해보았을 때 아무 문제가 안 생기는데, 여기서 어떤 제약이 있다고 하는 걸까요?

PickOne 커스텀 유틸리티 타입 구현하기

🔍 내용 (168쪽)

  • 옵셔널 + undefined로 타입을 지정하면 사용자가 의도적으로 undefined 값을 넣지 않는 이상, 원치 않는 속성에 값을 넣었을 때 타입 에러가 발생할 것이다.
{ account: string; card?: undefined} | { account?: undefined; card: string}

Promise.all을 사용할 때 NonNullable 적용하기

🔍 내용 (172쪽)

  • Array<AdCampaign[] | null>로 추론된다. NonNullable을 사용해서 필터링하면 Array<AdCampaign[]>로 추론할 수 있게 된다.

무한한 키를 집합으로 가지는 Record

🔍 내용 (179쪽)

type Category = string;
const foodByCategory: Record<Category, Food[]> = {
한식: [{ name: "제육덮밥" }, { name: "콩나물국밥" }],
일식: [{ name: "회", name: "텐동" }],
};

foodByCategory["양식"].map((food) => food.name); // 런타임에서 undefined가 되어 오류 반환
type PartialRecord<K extends string, T> = Partial<Record<K, T>>;

const foodByCategory: PartialRecord<Category, Food[]> = {
한식: [{ name: "제육덮밥" }, { name: "콩나물국밥" }],
일식: [{ name: "회", name: "텐동" }],
};

foodByCategory["양식"].map((food) => food.name); // Object is possibly `undefined`