제네릭과 유틸리티 타입

Dongwon Ahn·2024년 2월 21일
0

JS & TS 학습

목록 보기
5/7
post-thumbnail
post-custom-banner

제네릭이란?

제네릭 타입이란 함수나 클래스의 선언시점이 아닌, 사용 시점에 타입을 선언할 수 있는 방법을 제공합니다.
즉 제네릭은 코드를 작성할 때가 아니라 코드를 수행될 때(런타임) 타입을 명시합니다.
식별자를 써서 아직 정해지지 않는 타입을 표시합니다. (일반적으로 T, K, U 등을 사용)

제네릭 사용 이유

  • 재사용성이 높은 함수와 클래스를 생성할 수 있습니다.
  • 오류를 쉽게 잡을 수 있습니다.
    • 제네릭을 사용하지 않고, any 타입을 사용하면 여러 타입을 넣을 수 있습니다.
    • 타입 체크를 진행하지 않기 때문에 타입 힌트나 컴파일단계에서 에러를 잡을 수 없게 됩니다.

사용법

제네릭은 함수의 파라미터를 넣는 것과 같이 사용합니다.

// 어떤 타입을 받을 건지 먼저 정의합니다. (const logText = <T>())
// params 타입으로 정의 (text: T)
const logText = <T>(text: T): T => {
  console.log(text);
  return text;
}
// 함수를 호출할 때 타입 정의
const str = logText<string>('hello');
str.split("") // string 으로 정의했기 때문에 문자열 함수인 split 가능
const arr = logText<number[]>([1]);
arr.map((a) => a) // number 배열로 정의했기 때문에 배열 함수인 map 가능

제네릭 사용 예시

  • react table 컬럼 및 data 제네릭 사용
// TableColumn 인터페이스 정의: 각 컬럼에 대한 정보를 명시합니다.
interface TableColumn<T> {
  title: string; // 테이블 헤더에 표시될 컬럼 이름
  key: keyof T; // 데이터 객체에서 해당 컬럼의 값을 찾기 위한 키
  render?: (value: any, record: T) => React.ReactNode; // 커스텀 렌더링 함수 (옵션)
}

// GenericTable 컴포넌트 정의
interface GenericTableProps<T> {
  columns: TableColumn<T>[]; // 컬럼 정보 배열
  data: T[]; // 표시할 데이터 배열
}

// params를 제네릭으로 활용
function GenericTable<T>({ columns, data }: GenericTableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => (
            <th key={String(column.key)}>{column.title}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((record, index) => (
          <tr key={index}>
            // 생략 ...
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// 데이터
const users = [
  { id: 1, name: 'John Doe', age: 30 },
  { id: 2, name: 'Jane Doe', age: 25 },
];

// 컬럼 정의
const columns = [
  { title: 'ID', key: 'id' },
  { title: 'Name', key: 'name' },
  { title: 'Age', key: 'age' },
];


function App() {
  return <GenericTable columns={columns} data={users} />;
}

유틸리티 타입

Typescript은 type을 쉽게 구성하기 위해 위에서 설명한 제네릭을 통해 lib.es5.d.ts에 미리 여러 유틸리티 타입을 만들어놨습니다. 자주 사용되는 몇몇 유틸리티 타입에 대해 설명 및 해당 타입이 어떻게 구현됐는지 정리해보겠습니다.

Partial<T>

Partial<T> 타입의 모든 속성을 옵셔널으로 변경한 새로운 타입을 반환합니다.

// 구현된 방식
// T에 속한 모든 속성(P)에 ? 연산자를 통해 옵셔널로 변경
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// 사용 예시
interface Person {
	name: string;
  	nickname: string;
}
type PartialPerson = Partial<Person>;
const me: PartialPerson = { name: "안동원" } // 닉네임 없이 사용 가능

위 예시에 나와있는 선언된 PartialPerson은 아래 이미지와 같이 타입이 선언됩니다.

Required<T>

Required<T>타입은 Partial<T>와 반대로 모든 속성을 옵셔널에서 필수로 변경한 새로운 타입을 반환합니다.

// 구현된 방식
// T에 속한 모든 속성(P)에 -? 연산자를 통해 옵셔널 제거
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// 사용 예시
interface Developer {
  name: string;
  skill?: string;
}

type TDeveloper = Required<Developer>;

const developer: TDeveloper = { name: '안동원', skill: 'typescript' };

위 예시에 나와있는 TDeveloper은 아래 이미지와 같이 타입이 선언됩니다.

Readonly<T>

Readonly<T>는 모든 속성을 읽기 전용으로 변경한 새로운 타입을 반환합니다.

// 구현된 방식
// T에 속한 모든 속성(P)에 readonly 키워드를 추가
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 사용 예시
type TNotice = {
  id: number;
  title: string;
  content: string;
};

const notice: Readonly<TNotice> = {
  id: 1,
  title: 'title',
  content: 'content',
};

// 읽기 전용이기 때문에 에러 발생
notice.id = 2; // Error: Cannot assign to 'id' because it is a read-only property

Record<K,T>

Record<K,T>는 제네릭의 key를 속성으로, 제네릭의 Type를 속성값을 타입으로 지정하는 새로운 타입을 반환합니다.

// 구현된 방식
// K의 속성을 새로운 타입의 속성이 되며, T는 타입이 됩니다.
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

// 사용 예시
type TPlatformType = 'android' | 'ios' | 'web';
type RecordPlatformType = Record<TPlatformType, string>;

// TPlatformType의 값이 속성 값이 됨
const platformType: RecordPlatformType = {
  android: '안드로이드',
  ios: '아이폰',
  web: '웹',
};

Pick<T, K>

Pick<T, K>은 제네릭의 타입으로 부터 제네릭의 Key에 해당하는 속성을 선택하여 따로 모아 타입을 반환합니다.

// 구현된 방식
// K extends keyof T 
// K는 타입 T의 필드만 가질 수 있는 합집합임을 의미합니다
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

// 사용 예시
interface IUser {
  id: number;
  name: string;
  profile: string;
}

type TUserProfile = Pick<IUser, 'id' | 'profile'>;

const userProfile: TUserProfile = {
  id: 1,
  profile: 'profile',
};

Exclude<Type1, Type2>

Exclude<T,U> 는 Type1에서 Type2를 제외한 나머지 타입을 반환합니다.

// 구현된 방식
type Exclude<T, U> = T extends U ? never : T;

// 사용 예시
type TNoticeType = 'general' | 'important' | 'system' | 'event' | 'update';
type ExcludeNoticeType = Exclude<TNoticeType, 'update' | 'system'>;

const notice1: ExcludeNoticeType = 'general';
const notice2: ExcludeNoticeType = 'important';
const notice3: ExcludeNoticeType = 'event';
// const notice4: ExcludeNoticeType = 'update'; // 에러

예시로 나와있는 ExcludeNoticeType은 아래 이미지와 같이 선언됩니다.

Omit<T, K>

Pick<T,K> 타입과 반대로 Omit<T,K> 타입은 제네릭 key에 해당하는 속성을 제외한 나머지를 따로 모아 새로운 타입을 반환합니다.

// 구현된 방식
// Pick과 Exclude를 활용하여 구현
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 사용 예시
interface IUser {
  id: number;
  name: string;
  profile: string;
}

type TUser = Omit<IUser, 'profile'>;

const userProfile: TUser = {
  id: 1,
  name: '안동원',
};

profile
Typescript를 통해 풀스택 개발을 진행하고 있습니다.
post-custom-banner

0개의 댓글