ts 책 스터디(고급타입)

MountionRiver·2025년 7월 15일

타입스크립트 독자 시스템

  • 타입스크립트는 자바스크립트의 슈퍼셋이다.

any

any는 자바스크립트에 존재하는 모든 값을 오류 없이 받을 수 있다. 즉 자바스크립트의 기본 사용방식과 같으므로 타입을 명시하지 않은 것과 같은 효과. 즉 타입스크립트에서의 any는 지양해야 하는 패턴이다. 그럼에도 불구하고 any를 사용해야 하는 경우가 있다.
1. 개발 단계에서 임시로 값을 지정해야 할 경우

  • 매우 복잡한 구성요소로 이루어져 있으며 개발 과정에서 추후 값이 변경 될 가능성이 있거나, 아직 세부 항목에 대한 타입이 확정되지 않은 경우. 하지만 any를 남발 할 경우 타입 안정성을 저해하며, 추후 다른타입으로 변경하는 과정이 누락 될 경우 문제가 발생 할 수 있다.
  1. 어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때
  • 자바스크립트 입장에서 어떤 값의 타입을 명확하게 지정하기 어려운 상황.
    - 예를들어 api 요청 응답 처리, 백 함수 전달, 타입이 잘 정제되지 않아 파악이 힘든 라이브러리 등. 콜
  1. 값을 예측 할 수 없을 때 암묵적으로 사용
  • 외부 라이브러리나 웹 api 요청에 따라서 다양한 값을 반환하는 api 대표적으로 fetchAPI

unknown

  • any와 유사한 모든 타입의 값이 할당 될 수 있다.
    하지만 다른 타입으로 선언된 변수에는 unknown 타입의 값을 할당 할 수 없다.

  • any 타입의 값에 unknown을 넣을수 있으나 다른 타입의 값에 unknown 타입의 값을 넣을 수는 없다.

  • unknown은 타입스크립트 3.0때 릴리즈 됨. 기존 타입시스템의 부족한 점을 보완하기 위해서 등장. unknown을 할당 시에 에러가 발생하지 않으나 런타임 시에 에러가 발생했다.

  • 짧게 이야기 하면 상위에서 타입을 알 수 없지만, 자식(하위) 컴포넌트나 함수 내부에서 타입을 좁혀가며 안전하게 사용하기 위해서 만든 타입이라고 예상됨

void

  • 함수가 어던 값을 반환하지 않을 경우에 void를 지정해서 사용. 함수에서만 사용하는 것은 아니나 함수가 아닐 경우 대다수가 무의미하다. void 에는 undefined나 null만 할당 가능하다.

never

  • 값을 반환 할 수 없는 타입. 값을 반환하지 않는 것과 반환 할 수 없는 것은 명확히 해야함

값을 반환 할 수 없는 경우
1. 에러를 던지는 경우
자바스크립트에서는 런타임에 의도적으로 에러를 발생시키고 캐치 할 수 있다. 이는 값을 반환하는 것으로 간주 하지 않는다. 이때 never 타입이다.
2. 무한히 함수가 실행되는 경우
무한 루프일 경우 함수가 종료되지 않기 때문에 값을 반환하지 못한다.

타입스크립트에서 조건부 타입을 결정 할 때 특정 조건을 만족하지 않는 경우에 엄격한 타입 검사 목적으로 never를 사용하기도 한다.

Array

  • 배열타입을 가르키는 Array 키워드는 타입스크립트에서는 조금 다름.
    일반적으로 배열은 동적 언어의 특징에 따라서 어떤 값이든 배열의 원소로 허용함.
    하지만 타입스크립트의 정적 타입검사와는 맞지 않음
    일반적인 정적타입의 언어(java 혹은 c++ 같은)언어는 배열을 선언 할 시에 배열의 크기와 타입을 동시에 제한한다. 타입스크립트에서는 명시적인 타입을 선언하여 해당 타입의 원소를 관리 하는 것을 말함.
    이러한 것은 두가지 방법으로 타입을 선언할 수 있으며 선언의 형식제외 다른 것은 없다. 하나로 통일하거나 두가지를 혼용해서 사용해도 큰 문제가 없다.

선언방식
1. [](대괄호)형식을 사용하여 배열타입을 선언

const array1:number[]: [1,2,3]
  1. Array 키워드를 통해서 배열 타입을 선언
const array2:Array<number> = [1,2,3]

이 두가지 방식이 존재하며, 여러 타입을 관리 할 경우 유니온 타입을 사용 할 수 있다.

유니온 타입을 사용할 경우

const array1:number[] | string[]: [1,2,"text"]
const array2:Array<number | string> = [1,2,"text"]
const array1:(number|string)[]: [1,2,"text"]

타입스크립트는 배열타입을 명시하는 것만으로는 길이를 제한 할 수 없다. 튜플은 배열의 하위 타입으로 기존의 타입스크립트의 배열 기능에 길이 제한 까지 추가한 타입 시스템이라고 볼 수 있다.

튜플 타입

const tuple1:[number] = [1] // 길이는 한개이며 요소로는 숫자타입 제한
const tuple2:[number, string, boolean] = [1,"text", false] // 여러 타입 혼합 가능

enum

  • 열거형이라고 불리며 타입스크립트에서 지원하는 특수 타입. 일종의 구조체를 만드는 타입 시스템.
    enum을 사용해서 열거형을 정의 할 수 있는데 enum 은 각각의 멤버를 가지고 있다. 이것들은 자바스크립트의 객체의 모양새를 닮았다. 타이스크립트는 명명한 각 메버의 값을 스스로 추론한다. 기본적인 추론 방식은 숫자 0부터 1씩 늘려가며 값을 할당하는 것이다.
enum ProgrammingLanguage {
  Typescript, // 0
  Javascript, // 1
  Java, // 2
  Python, // 3
  Kotlin, // 4
  Rust, // 5
  Go, // 6
}
// 각 멤버에게 접근하는 방식은 자바스크립트에서 객체의 속성에 접근하는 방식과 동일하다
ProgrammingLanguage.Typescript; // 0
ProgrammingLanguage.Rust; // 5
ProgrammingLanguage["Go"]; // 6

// 또한 역방향으로도 접근이 가능하다
ProgrammingLanguage[2]; // "Java"
  • 또한 각 멤버에 명시적으로 값을 할당 할 수 있다. 모든 멤버에 일일이 값을 할당할 수도 있지 만, 일부 멤버에 값을 직접 할당하지 않아도 타입스크립트는 누락된 멤버를 아래와 같은 방식 으로 이전 멤버 값의 숫자를 기준으로 1씩 늘려가며 자동으로 할당한다.
enum ProgrammingLanguage {
  Typescript = "Typescript",
  Javascript = "Javascript",
  Java = 300,
  Python = 400,
  Kotlin, // 401
  Rust, // 402
  Go, // 403
}
  • enum은 주로 문자열 상수를 생성하는데 사용 된다. 이를 통해 집합 구조체를 만들 수 있으며, 사용자 입장에서도 간편하게 활용 할 수 있다. 또한 열거형은 그 자체로 변수 타입으로 지정 할 수 있다. 이때 열거형을 타입으로 가지는 변수는 해당 열거형이 가지는 모든 멤버를 값으로 받을 수 있다.
enum ItemStatusType {
  DELIVERY_HOLD = "DELIVERY HOLD", // 배송 보류
  DELIVERY_READY = "DELIVERY READY", // 배송 준비 중
  DELIVERING = "DELIVERING", // 배송 중
  DELIVERED = "DELIVERED", // 배송 완료
}

const checkItemAvailable = (itemStatus: ItemStatusType) => {
  switch (itemStatus) {
    case ItemStatusType.DELIVERY_HOLD:
    case ItemStatusType.DELIVERY_READY:
    case ItemStatusType.DELIVERING:
      return false;
    case ItemStatusType.DELIVERED:
    default:
      return true;
  }
};

checkitemAvailable 함수의 인자인 itemStatus는 ItemStatusType 열거형을 타입으로 가진다. 이를 통해 얻을 수 있는 효과는 itemstatus의 타입이 문자열로 지정된 경우와 비교 했을 때 다음과 같다.

  • 타입 안정성: ItemStatusType에 명시되지 않은 다른 문자열은 인자로 받을 수 없다. 따라서 타입 안정성 이 우수하다.
  • 명확한 의미 전달과 높은 응집력: ItemStatusType 타입이 다루는 값이 무엇인지 명확하다. 아이템 상태에 대한 값을 모아놓은 것으로 응집력이 뛰어나다.
  • 가독성: 응집도가 높기 때문에 말하고자 하는 바가 더욱 명확하다. 따라서 열거형 멤버를 통해 어떤 상태를 나타내는지 쉽게 이해할 수 있다.
    (예: ItemStatusType.DELIVERY_HOLD vs DELIVERY_HOL)

열거형은 관련이 높은 멥러를 모아 문자열 상수처럼 사용하고자 할 때 유용하게 사용 할 수 있다. 하지만 열거형을 사용할 때 주의해야 할 점은 숫자로만 이루어져 있거나 타입스크립트가 자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수 있다.
이런한 동작을 막기 위해서 const enum으로 열거형을 선언하는 방법이 있다. 이 방식은 역방향으로의 접근을 허용하지 않기 때문에 자바스크립트에서 객체에 접근하는 것과 유사한 동작을 보장한다.

ProgrammingLanguage[200] // undefined 를 출력하지만 별다른 에러를 발생시키지 않는다.

// 아래와 같이 선언 하면 위와 같은 문제를 해결 할 수 있다.
const enum ProgrammingLanguage {
	// ....
}

그러나 이런 const enum 으로도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 막지 못한다. 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로의 접근을 방지한다. 따라서 문자열 상수 방식으로 열거형을 사용하는 것이 숫자 상수 방식보다 안저나며 의도하지 않은 접근을 방지하는데 도움이 된다.

타입조합

교차 타입

여러가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있다. 다시 말해 기존에 존재하는 여러가지 다른 타입들을 합쳐서 해당 타입의 모든 멤버를 가지는 새로운 타입을 생성하는 것. 교차 타입은 &을 사용해서 표기한다.

export type User = {
  id: number;
  email: string;
  nickname?: string;
  image?: string;
  createdAt: string;
  updatedAt: string;
};

export type PostUsers = Pick<User, "email" | "nickname"> & {
  password: string;
};

유니온 타입

교차 타입이 a와 b를 모두 만족하는 경우라면 유니온 타입은 a또는 b중 하나가 될수 있는 타입.

export type PaginationParams = {
  size?: number; // 한번에 불러오는 갯수
  page?: number; // 부르는 페이지
  sort?: string; // 데이터 정렬방식
  direction?: "desc" | "asc"; // 최신순 오래된순
};

인덱스 시그니쳐

특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 잇을 대 사용하는 문법
인터페이스의 내부에 [Key: K]: T 형식으로 명시해주면 되는데, 이는 해당 타입의 속성 키는 모두 K 타입이어야 하고 속성값은 모두 T 타입을 가져야 한다는 의미이다.

export type IndexSignatures = {
  [key: string]: number|boolean;
  length: number;
  isValid: boolean;
  name:string; // number이거나  boolean 타입이 아니므로 에러발생
}

인덱스드 엑세스 타입

다른 타입의 특정 속성이 가지는 타입을 조회하기 위해서 사용.

type Example = {
  a: number;
  b: string;
  c: boolean;
};

type IndexedAccess = Example["a"];
type IndexedAccess2 = Example["a" | "b"]; / / number | string
type IndexedAccess3 = Example[keyof Example]; / / number | string | boolean

type ExAlias = "b" | "c";
type IndexedAccess4 = Example[ExAlias]; / / string | boolean

또한 배열의 요소 타입을 조회하기 위해서 인덱스트 엑세스 타입을 사용하는 경우가 있다.

const PromotionList = [
{ type: "product", name: "chicken" },
{ type: "product", name: "pizza" },
{ type: "card", name: "cheer-up" },
] ;

type ElementOf<T> = typeof T[number];

// type PromotionItemType = { type: string; name: string }
type PromotionItemType = ElementOf<PromotionList>;

맵드 타입

다른 타입을 기반으로 한 타입을 선언 할 때 사용하는 문법. 인덱스 시그니쳐 문법을 사용해서 반복적인 타입선언을 효과적으로 줄일 수 있다.

type Example = {
  a: number;
  b: string;
  c: boolean;
};

type Subset<T> = {
  [K in keyof T]?: T[K];
}

const Example: Subset<Example> = {a: 3 };
const bExample: Subset ‹Example> = { b: "hello" };
const acExample: Subset«xample> = { a: 4, c: true };

맵드 타입을 사용할 때 readonly와 ?를 수식어로 적용 할 수 있다. 이러한 수식어를 더해주는 것 뿐만 아니라 제거할 수도 있다. 기존타입에 readonly와 ? 존재했다면 앞에 -를 붙여 주는 것으로 수식어를 제거한 타입을 선언 가능하다.

type ReadOnlyEx = {
  readonly a: number;
  readonly b: string ;
};

type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
}

type ResultType = CreateMutable«ReadOnlyEx>; // { a: number; b : string }

type OptionalEx = {
  a?: number;
  b?: string;
  c: boolean;

type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];

type ResultType = Concrete‹OptionalEx>; // { a: number; b : string ; c: boolean 

덧붙여 맵드 타입에서는 as 키워드를 사용하여 키를 재지정할 수 있다. BottomSheetStore의 키 이름에 BottomSheetMap의 키 이름을 그대로 쓰고 싶은 경우가 있을 수 있고, 모든 키에_BOTTOM_SHEET를 붙이는 식으로 공통된 처리를 적용하 여 새로운 키를 지정하고 싶을 수도 있다. 이럴 때는 아래 예시처럼 as 키워드를 사용해서 효율적으로 처리할 수 있다.

type BottomSheetStore = {
  [index in BOTTOM SHEET _ID as 'Sindex) _BOTTOM_SHEET']: {
    resolver?: (payload: any) => void;
    args?: any;
    isOpened: boolean;
  };
};

템플릿 리터럴 타입

  • 자바스트립트의 템플릿 문자열을 사용하여 문자열 리터럴 타입을 선언 할 수 있는 문법.
type Stage =
| "init"
| "select-image"
| "edit-image"
| "decorate-card"
| "capture-image";

type StageName = `$(Stage)-stage`;
// 'init-stage' | 'select-image-stage' | 'edit-image-stage' | 'decorate-card-stage' | 'capture-image-stage'

제네릭 타입

  • C나 JAVA 같은 정적타입 언어에서 다양한 타입간의 재사용성을 높이기 위해서 사용하는 문법.
  • 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고, 타입 변수를 사용해서 해당 위치를 비워 둔 다음, 실제로 그 값을 외부에서 타입변수 자리에 타입을 지정하여 사용하는 방식.

이렇게 하면 함수, 타입, 클래스 등 여러 타입에 대해 하나하나 따로 정의하지 않아도 되기 때문에 재사용성이 크게 향상된다. 타입 변수는 일반적으로 <T>와 같이 꺾쇠괄호 내부에 정의되며, 사용할 때는 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어주면 된다. 보통 타입 변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용한다.

type ExampleArrayType<T> = T[];
const array1: ExampleArrayTypes<string> = ["치킨","피자",우동"] ;
  • 앞서 제네릭이 일반화된 데이터 타입을 말한다고 했는데, 이 표현만 보면 any의 쓰임과 혼동 할 수도 있을 것이다. 하지만 둘은 명확히 다르다.
    둘의 차이는 배열을 떠올리면 쉽게 알 수 있다. any 타입의 배열에서는 배열 요소들의 타입이 전부 같지 않을 수 있다.
    쉽게 말해 타입 정보를 잃어버린다고 생각하면 편하다. 즉, any를 사용하면 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급된다.
    반면에 제네릭은 any처럼 아무 타입이나 무분별하게 받는 게 아 니라, 배열 생성 시점에 원하는 타입으로 특정할 수 있다.
    다시 말해 제네릭을 사용하면 배열 요소가 전부 동일한 타입이라고 보장할 수 있다.
type ExampleArrayType2 = any|];
const array2: ExampleArrayType2 = [
  "치킨",
  {
    id: 0,
    name: "치킨",
    price: 20000,
    quantity: 1,
  }
  99,
  true,
};

참고로 제네릭 함수를 호출할 때 반드시 꺾쇠괄호(<>) 안에 타입을 명시해야 하는 것은 아니다.
타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해준다.
따라서 타입 추론이 가능한 경우에는 타입 명시를 생략할 수 있다.

function exampleFunc<T>(arg: T): T[] {
  return new Array(3).fill(arg);
}

exampleFunc("hello"); // T는 string으로 추론된다

또한 특정 요소 타입을 알 수 없을 때는 제네릭 타입에 기본값을 추가할 수 있다.

interface SubmitEvent<T = HTMLElement>extends SyntheticEvnet<T>{ submitter: T;
}

제네릭은 일반화 된 데이터 타입이다. 따라서 함수나 클래스 등의 내부에서 제네릭을 사용할 때 어떤 타입이든 될 수 있다는 개념을 알고 있어야 한다. 특정한 타입에서만 존재하는 멤버를 참조하려고 하면 안된다. 예를 들어 배열에만 존재하는 Length 속성을 제네릭에서 참조하려고 하면 당연히 에러가 발생한다. 컴파일러는 어떤 타입 이 제네릭에 전달될지 알 수 없기 때문에 모든 타입이 Length 속성을 사용할 수는 없다고 알 려주는 것이다.

function exampleFunc2<T>(arg: T): number {
  return arg.length; // 에러발생: Property 'length' does not exist on type 'T'
}

이럴 때는 제네릭 꺾쇠괄호 내부에 'Length 속성을 가진 타입만 받는다'라는 제약을 걸어줌으 로써 Length 속성을 사용할 수 있게끔 만들 수 있다.

interface TypeWithLength {
  length: number;
}

function exampleFunc2<T extends TypeWithLength>(arg: T): number {
  return arg.length;
}

제네릭을 사용할 때 주의해야 할 점이 있다. 파일 확장자가 tsx일 때 화살표 함수에 제네릭을 사용하면 에러가 발생한다. tsx는 타입스크립트 + JSX이므로 제네릭의 꺾쇠괄호와 태그의 꺾 쇠괄호를 혼동하여 문제가 생기는 것.

이러한 상황을 피하기 위해서는 제네릭 부분에 extends 키워드를 사용하여 컴파일러에게 특정 타입의 하위 타입만 올 수 있음을 확실히 알려주면 된다. 보통 제네릭을 사용할 때는 function 키워드로 선언하는 경우가 많다.

// 에러발생: JSX element 'T' has no corresponding closing tag
const arrowExampleFunc = <T>(arg: T): T[] => {
  return new Array(3).fill(arg);
}
// 에러 발생 X
const arrowExampleFunc2 = <T extends >(arg: T): T[]=> {
  return new Array(3).fill(arg);
} 

제네릭 사용법

함수의 제네릭

어떤 함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을때 제네릭을 사용 할 수 있다. 아래 예시처럼 T 자리에 넣는 타입에 따라 ReadOnlyRepository가 적절히 사용 될 수 있다.

function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string):
Repository<T> {
  return getConnection("ro").getRepository(target);
}

호출 시그니쳐의 제네릭

호출 시그니처는 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입 을 미리 선언하는 것을 말한다. 호출 시그니처를 사용함으로써 개발자는 함수 호출 시 필요한 타입을 별도로 지정할 수 있게 된다. 호출 시그니처를 사용할 때 제네릭 타입을 어디에 위치시 키는지에 따라 타입의 범위와 제네릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있다.

interface useSelectPaginationProps<T> {
  categoryAtom: RecoilState<number>;
  filterAtom: RecoilState<string[]>; sortAtom:
  RecoilState<SortType>;
  fetcherFunc: (props: CommonListRequest) => Promise<DefaultResponse<ContentListRes ponse<T>>>;
} 

이 코드는 우아한형제들 배민선물하기팀의 호출 시그니처 제네릭 활용 예시다. 여기서 는 useseLectPaginationProps의 타입 별칭으로 한정했다. 따라서 useSelectPagination-Props을 사용할 때 타입을 명시함으로써 제네릭 타입을 구체 타입으로 한정한다. 설명을 덧 붙이면 useSeLectPaginationProps가 사용되는 useSelectPagination 훅의 반환 값도 인 자에서 쓰는 제네릭 타입인 T와 연관 있기 때문에 이처럼 작성했다.
그다음 배민커머스웹프론트개발팀의 활용 예시를 보자.

export type UseRequesterHookType = ‹ RequestData = void, ResponseData = void>( baseURL?:
string | Headers,
defaultHeader?: Headers
) => [RequestStatus, Requester RequestData, ResponseData>];

이 예시에서 <RequestData, ResponseData>는 호출 시그니처의 일부, 다시 말해 괄호(())
앞에 선언했기 때문에 타입스크립트는 UseRequesterHookType 타입의 함수를 실제 호출할 때 제네릭 타입을 구체 타입으로 한정한다.

배민커머스웹프론트개발팀은 프로젝트 구조를 따르기 위해 아래처럼 작성했다.

function useSelectPagination<T extends CardListContent | CommonProductResponse>({
  categoryAtom,
  filterAtom,
  sortAtom,
  fetcherFunc,
}: useSelectPaginationProps<T>): {
  intersectionRef: RefObject<HTMLDivElement>;
  data: T[];
  categoryId: number;
  isLoading: boolean;
  isEmpty: boolean;
} {
// ...
 
  return {
    intersectionRef,
    data: swappedData ?? [],
    isLoading,
    categoryId,
    isEmpty,
  };
}

제네릭 클래스

제네릭 클래스는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스이다. 제네릭 클래스는 다음과 같은 형태로 선언된다.

class LocalDB<T> {
  // ...
  async put(table: string, row: T): Promise<T> {
    return new Promise<T>((resolved, rejected) => {/* T 타입의 데이터를 DB에 저장 */});
  }
  async get(table:string, key: any): Promise<T> {
    return new Promise<T>((resolved, rejected)=> {/* T 타입의 데이터를 DB에서 가져옴 */});
  }
  async getTable(table: string): Promise<T[]> {
    return new Promise<T[]>(resolved, rejected)=> {/* TO 타입의 데이터를 DB에서 가져
옴*/ });
  }
}
export default class IndexedDB implements ICacheStore {
  private _DB?: LocalDB<{ key: string; value: Promise‹Record<string, unknown>;
cacheTTL: number }>;

  private DB() {
    if (!this. DB) {
      this._DB = new LocalDB("localCache", { ver: 6, tables: [{ name: TABLE_NAME, keyPath: "key" }] });
    }
    return this._DB;
  }
// ...
}

클래스 이름 뒤에 타입 매개변수인 를 선언해준다. 는 메서드의 매개변수나 반환 타 입으로 사용될 수 있다. LocaLDB 클래스는 외부에서 { key: string; value: Promise<Record<string, unknown>>; cacheTTL: number } 타입을 받아들여 클래스 내부에 서 사용될 제네릭 타입으로 결정된다.
제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용된다. 특정 메서드만을 대상으로 제네릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 된다.

제한된 제네릭

타입 매개변수에 대한 제약 조건을 설정하는 기능
예를 들어 string 타입으로 제약하려면 타입 매개변수는 특정 타입을 상속해야 한다.

  type ErrorRecord<Key extends string>= Exclude<key, ErrorCodeType> extends never ? Partial<Record<Key, boolean>> : never;

이처럼 타입 매개변수가 특정 타입으로 묶였을 때 키를 바운드 타입 매개변수라고 부른다. 그리고 string을 키의 상한 한계라고 한다.
상속 받을수 있는 타입으로 기본 타입뿐만 아니라 상황에 따라 인터페이스나 클래스도 사용 할 수 있다. 또한 유니온 타입을 상속해서 선언 할 수 있다

function useSelectPagination<T extends CardListContent | CommonProductResponse>({
  filterAtom,
  sortAtom, 
  fetcherFunc,
}: useSelectPaginationProps<T>): {
  intersectionRef: RefObject<HTMLDivElement>;
  data: T[];
  categoryld: number;
  isLoading: boolean;
  isEmpty: boolean;
}{
  // ...
}

// 사용하는 쪽 코드
const { intersectionRef, data, isLoading, isEmpty } = useSelectPagination<CardListContent>({
  categoryAtom: replyCardCategoryIdAtom,
  filterAtom: replyCardFilterAtom,
  sortAtom: replyCardSortAtom,
  fetcherFunc: fetchReplyCardListByThemeGroup,
});

확장된 제네릭

여러 타입을 상속받을 수 있으며 타입 매개변수를 여러개 둘 수도 있다 .

<Key extends string>

타입을 이런 식으로 제약해버리면 제네릭의 유연성을 잃어버린다. 제네릭의 유연성을 잃지 않 으면서 타입을 제약해야 할 때는 타입 매개변수에 유니온 타입을 상속해서 선언하면 된다.

<Key extends string | number>

유니온 타입으로 T가 여러 타입을 받게 할 수는 있지만, 타입 매개변수가 여러 개일 때는 처리 할 수 없다. 이럴 때는 매개변수를 하나 더 추가하여 선언한다.
다음은 Ok 타입이나 Err 타입을 매개변수 인자로 받아 사용하는 예시이다.

export class APIResponse«Ok, Err = string> {
  private readonly data: Ok | Err | null;
  private readonly status: ResponseStatus;
  private readonly statusCode: number | null;

  constructor (
    data: Ok | Err | null,
    statusCode: number | null,
    status: ResponseStatus
  ) {
    this.data = data;
    this.status = status;
    this.statusCode = statusCode;
  }

public static Success<T, E = string>(data: T): APIResponse<T, E> {
  return new this<T, E>(data, 200, ResponseStatus.SUCCESS);
}
                  
public static Error<T, E = unknown>(init: AxiosError): APIResponse<T, E> {
  if (linit.response) {
    return new this<T, E>(null, null, ResponseStatus.CLIENT_ERROR);
  }

  if (linit.response.data?.result) {
    return new this<T, E>(
    null,
    init.response.status,
    ResponseStatus.SERVER_ERROR
  );
}

  return new this<T, E>(
    init.response.data.result,
    init.response.status,
    ResponseStatus.FAILURE
  )
}
// ...
}

// 사용하는 쪽 코드
const fetchShopStatus = async (): Promise<
  APIResponse<IShopResponse | null>> => {
  // ...

  return (await API.get<IShopResponse | null>("/v1/main/shop", config)).map((it) => it.result);
};

제네릭 예시

제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다.
현업에서 가장 많이 제네릭이 활용할 때는 API 응답 값의 타입 을 지정할 때이다.

우아한형제들에서는 API 응답 값의 타입을 지정할 때 제네릭을 활용하여 적절한 타입 추론과 코드의 재사용성을 높이고 있다.

export interface MobileApiResponse<Data> {
  data: Data;
  statusCode: string;
  statusMessage?: string;
}

이 코드를 살펴보면 API 응답 값에 따라 달라지는 data를 제네릭 타입 Data로 선언하고 있 다. 이렇게 만든 MobileApiResponse는 실제 API 응답 값의 타입을 지정할 때 아래와 같이 사용되고 있다.

export const fetchPriceInfo = 0: Promise<MobileApiResponse<PriceInfo>> => { 
  const priceUrl = "https: ~"; // url 주소

  return request({
    method: "GET",
    url: priceUrl,
  });
};

export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
  const orderUrl = "https: ~"; // url 주소

  return request({
    method: "GET",
    url: orderUrl,
  });
};

이처럼 다양한 API 응답 값의 타입에 MobileApiResponse을 활용해서 코드를 효율적으로 재사용할 수 있다.

이런 식으로 제네릭을 필요한 곳에 사용하면 가독성을 높이고 코드를 효율적으로 작성할 수 있다. 하지만 굳이 필요하지 않은 곳에 제네릭을 사용하면 오히려 독이 되어 코드를 복잡하게 만든다.

제네릭을 굳이 사용하지 않아도 되는 타입

제네릭이 필요하지 않을 때도 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다. 다음은 제네릭이 굳이 필요하지 않은데도 사용한 예시다.

type GType<T> = T;
type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
interface Order {
  getRequirement(): GType<RequirementType>;
}

GType이 다른 곳에서는 사용되지 않고 getRequirement 함수의 반환 값 타입으로만 사용되 고 있다고 가정해보자.

GType이라는 이름이 현재 사용되고 있는 목적의 의미를 정확히 담고 있지도 않을뿐더러 굳이 제네릭을 사용하지 않고 타입 매개변수를 그대로 선언하는 것과 같은 기능을 하고 있다.

즉, 아래처럼 사용하는 것과 동일하다.

type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
interface Order {
  getRequirement(): RequirementType;
}

any 사용하기

제네릭은 코드의 재사용성을 높이고 타입 추론을 하는 데 사용된다. 그러나 any를 사용하면 제네릭의 장점과 타입 추론 및 타입 검사를 할 수 있는 이점을 누릴 수 없게 된다. any 타입은 모든 타입을 허용하기 때문에 사실상 자바스크립트와 동일한 방식으로 코드를 작성하는 것과 같다. 따라서 any를 사용하면 제네릭을 포함해 타입을 지정하는 의미가 사라지게 된다.

type ReturnType<T = any> = {
// ...
};

가독성을 고려하지 않은 사용

제네릭이 과하게 사용되면 가독성을 해치기 때문에 코드를 읽고 타입을 이해하기가 어려워진다. 부득이한 상황을 제외하고 복잡한 제네릭은 의미 단위로 분할해서 사용하는 게 좋다.

ReturnType<Record<OrderType,Partial<Record<CommonOrderStatus | CommonReturnStatus,
Partial<Record<OrderRoleType, string[]>>>>>>>;

type CommonStatus = CommonOrderStatus | CommonReturnStatus;

type PartialOrderRole = Partial<Record<OrderRoleType, string[]>>;

type RecordCommonOrder = Record<CommonStatus, PartialOrderRole>;

type RecordOrder = Record<OrderType, Partial<RecordCommonOrder>>;

ReturnType‹RecordOrder>;

만약에 내가 작성한 코드를 다른 개발자가 쉽게 이해하지 못하고 있다면 혹시 제네릭을 오남 용하고 있는 것은 아닌지 검토해봐야 한다.

0개의 댓글