웹개발/TypeScript

TypeScript - Generic

bergerac 2025. 4. 25. 09:11

제네릭이란?

  • 재사용 가능한 컴포넌트(코드단위)를 만드는데 사용하며 데이터 데이터 타입을 미리 지정하지 않고 사용 시점에 데이터 타입을 설정한다
  • 코드의 중복을 줄이고 타입 안정성을 보장한다. 런타임이 아닌 컴파일 시점에 타입을 체크하기 때문이다.

제네릭 기본 문법

제네릭은 변수, 함수, 클래스, 인터페이스 등 모든 컴포넌트에 적용가능하다.

function generic<T>(arg: T): T {
  return arg;
}

generic<string>('hello'); // hello
generic<number>(345);     // 345
  • <T>는 타입 변수를 의미하며, 일반적으로 T, U, V 등 대문자 하나를 사용
  • 사용 시점에 타입을 명시하거나, 인자로 전달된 값을 기준으로 자동 추론됨

여러 타입변수 사용하기

하나의 컴포넌트에서 여러 타ㅣㅂ을 다룰 수 있도록 <T, U>처럼 여러 타입 변수를 동시에 활용할 수 있다.

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

 

제네릭 함수+타입 단언 활용

u를 T[]로 단언함으로서 유연한 타입 캐스팅이 가능하다

단, 이런 방식은 런타임 오류 가능성도 내포하므로 신중히 사용해야 한다.

function pair2<T, U>(first: T, second: T): U {
  return [first, second] as U;
}

 

제네릭 함수 예시

제네릭함수는 다양한 타입의 배열을 받아 동일한 로직을 수행할 수 있다.

function genericFunc<T>(args: T[]): T[] {
  console.log(`배열의 길이: ${args.length}`);
  return args;
}

genericFunc<boolean>([true, false, true]); // 배열의 길이: 3

 

제네릭 인터페이스

함수 타입의 인터페이스에도 제네릭을 적용할 수 있으며 이를 통해 다양한 타입의 함수구현을 하나의 툴로 만들 수 있다.

interface IGeneric<T> {
  (key: T): T;
}

 

제네릭 클래스

클래스 내부에서 여러 타입을 유연하게 적용 가능하다.

class GenericClass<T> {
  value: T;
  add: (x: T, y: T) => T;

  constructor(value: T, addFunc: (x: T, y: T) => T) {
    this.value = value;
    this.add = addFunc;
  }
}

const myGenericNumber = new GenericClass<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(4, 6)); // 10

 

예제 - 배열 뒤집기

any[]):any{}로 처리하면 타입 안정성이 떨어지지만 이렇게 제네릭을 활용하여 처리하면 타입 안정성을 놓치지 않기도 하며 배열 뒤집기가 가능하다. 이때 array안에 어떤 타입이 들어오는지에 따라 타입을 추론을 해준다.

function reverseArray<T>(array: T[]): T[] {
  return array.reverse();
}

reverseArray(['1', '2', '3']); // ['3', '2', '1']
reverseArray([true, false]);   // [false, true]

 

제네릭 인터페이스

k, v 두개의 타입 변수를 활용하여 키와 값을 모두 유연하게 설정 가능하다.

interface KeyValue<K, V> {
  key: K;
  value: V;
}

const keyValue1: KeyValue<string, number> = {
  key: 'bergerac',
  value: 123
};

const keyValue2: KeyValue<boolean, string> = {
  key: true,
  value: 'roxan'
};

 

제네릭의 제약 조건(extends)

T는 반드시 length속성을 가져야 하며, 그렇지 않으면 타입 에러가 발생한다. 

interface ILength {
  length: number;
}

function constraints<T extends ILength>(arg: T): void {
  console.log(arg.length);
}

 

고급기능 -keyof 연산자

객체 타입의 속성명을 문자열 리터럴 타입으로 추출한다.

type Type = { name: string; age: number };
type Union = keyof Type; // "name" | "age"

 

조건부 타입

T가 string 이면 'string'을 아니면 'not a string을 리턴하는 리터럴 타입

마치 타입 시스템 안에서 if-else문처럼 사용한다.

type Check<T> = T extends string ? 'String' : 'Not a String';

function checkType<T>(value: T): Check<T> {
  return typeof value === 'string' ? 'String' as Check<T> : 'Not a String' as Check<T>;
}

 

 

유니온 타입과 제네릭의 결합

유니온 타입은 이전 https://bergerac.tistory.com/74 글에서 나와있듯 둘중 하나의 타입이 될 수 있는 타입을 의미한다

T extends StringOrNumber는 타입 변수 T가 문자열 또는 숫자만 받을 수 있게 제약한다.

이런 방식은 조건별 분기 처리가 가능해, 타입별 동작을 쉽게 구현할 수 있다.

type StringOrNumber = string | number;

function unionGeneric<T extends StringOrNumber>(value: T) {
  if (typeof value === 'string') {
    return value.toLowerCase();
  }
  return value;
}

unionGeneric('Hello'); // hello
unionGeneric(1000);    // 1000

 

제네릭 유틸리티 타입

유틸리티 타입은 타입스크립트에서 기본 제공하는 제네릭 타입으로, 기존 타입을 변형하거나 재활용할때 매우 유용하다.

1. Partial<T> 일부 속성만 선택적으로

 

  • Partial<T>는 모든 속성을 **선택적(optional)**으로 만들어 준다
  • 예를 들어 Partial<User>는 { name?: string; age?: number }와 동일한 구조가 된다
  • 이걸 활용하면 특정 필드만 수정하는 함수에서 타입을 간결하게 처리할 수 있다

 

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

function updateUser(id: number, changes: Partial<User>) {
  const user = users[id];
  if (!user) {
    console.log('해당 id의 사용자는 없습니다.');
    return;
  }
  users[id] = { ...user, ...changes };
}
updateUser(1, { name: 'roxan' }); // name만 수정
updateUser(2, { age: 30 });       // age만 수정

 

2. Readonly<T> 모든 속성을 읽기 전용으로

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

const user: Readonly<Person> = {
  name: 'admond',
  age: 25
};

 

  • Readonly<T>는 타입 T의 모든 속성을 읽기 전용으로 바꾼다
  • 값을 수정하려고 하면 컴파일 타임에 에러 발생
  • 불변성을 보장해야 할 때 유용하게 활용된다

3. Omit<T, K> 특정 속성을 제외한 타입 생성

  • Omit<T, K>는 타입 T에서 속성 K를 제외한 새로운 타입을 만들어준다
  • Omit<Employee, 'position'>은 position을 제외한 나머지 속성만 포함하게 된다
interface Employee {
  id: number;
  name: string;
  age: number;
  position: string;
}

type EmployeeWithoutPosition = Omit<Employee, 'position'>;

 

const newEmployee: EmployeeWithoutPosition = {
  id: 1,
  name: 'Christian',
  age: 25
};

기존 타입을 기반으로 부분적인 구조를 만들고자 할 때 유용하다.