[TypeScript] 제네릭(Generics) 개념정리

2025. 3. 1. 21:02·TypeScript
728x90

제네릭이란?

💡 제네릭은 타입스크립트에서 함수, 클래스, 인터페이스 등을 다양한 타입에서 재사용할 수 있도록 해주는 템플릿이다. 💡
즉, 코드를 작성할 때 타입을 확정하지 않고, 사용하는 시점에 원하는 타입을 지정할 수 있게 해준다.

 

제네릭을 사용하면 "any" 타입을 사용할 때처럼 타입 유연성을 가질 수 있으면서도, 컴파일 시점에 타입 체크를 할 수 있어 타입 안전성도 확보할 수 있다. 이것이 제네릭의 가장 큰 장점이라고 생각한다!

 

제네릭은 꺾쇠괄호(<>)를 사용하여 표현하며, 주로 T, E, K, V 등의 단일 대문자를 사용하는 것이 관례다.

(물론 다른 이름을 사용해도 무방하다.)


제네릭 함수

가장 먼저 살펴볼 것은 제네릭 함수다!

예를 들어, 어떤 값을 받아서 그대로 반환하는 간단한 함수를 만들어보자.

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

// 사용 예시
const num = identity<number>(5); // 명시적으로 타입 지정
const str = identity("Hello"); // 타입 추론으로 string 타입으로 결정됨

위 코드에서 identity 함수는 제네릭 T를 사용한다.

이 함수는 T 타입의 인자를 받아서 같은 T 타입을 반환한다.

함수를 호출할 때 타입을 명시적으로 지정하거나, 전달하는 인자에 따라 타입스크립트가 타입을 추론하게 할 수 있다.

 

이렇게 제네릭을 사용하면 다양한 타입에 대해 동작하는 함수를 타입 안전하게 만들 수 있다.

만약 제네릭 없이 구현한다면 any 타입을 사용해야 할 텐데, 그러면 타입 정보가 손실되어 타입 안전성이 떨어진다.

 

좀 더 실용적인 예제를 보자. 배열에서 첫 번째 요소를 반환하는 함수를 만들어보겠다.

function getFirstElement<T>(arr: T[]): T | undefined {
    return arr.length > 0 ? arr[0] : undefined;
}

// 사용 예시
const numbers = [1, 2, 3, 4, 5];
const firstNumber = getFirstElement(numbers); // 타입은 number | undefined

const names = ["Alice", "Bob", "Charlie"];
const firstName = getFirstElement(names); // 타입은 string | undefined

const mixed = [1, "two", true];
const firstMixed = getFirstElement(mixed); // 타입은 number | string | boolean | undefined

이 함수는 어떤 타입의 배열이든 받아서 그 배열의 첫 번째 요소를 반환한다.

빈 배열일 경우를 대비해 undefined도 반환 타입에 포함시켰다.

타입스크립트는 전달된 배열의 타입을 자동으로 추론하여 반환 타입을 결정해준다.


제네릭 인터페이스

함수뿐만 아니라 인터페이스에서도 제네릭을 사용할 수 있다.

이를 통해 다양한 타입에 대해 동일한 구조를 가진 인터페이스를 정의할 수 있다.

interface Box<T> {
    value: T;
    getValue(): T;
}

// 구현 예시
class NumberBox implements Box<number> {
    constructor(public value: number) {}
    
    getValue(): number {
        return this.value;
    }
}

const box = new NumberBox(42);
console.log(box.getValue()); // 42

위 코드에서 Box 인터페이스는 제네릭 타입 T를 사용한다.

이 인터페이스를 구현하는 클래스는 특정 타입을 지정해야 한다. (위 예시에서는 NumberBox가 Box<number>를 구현하고 있음)

 

여러 제네릭 타입 매개변수를 사용하는 것도 가능하다.

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

const pair1: Pair<string, number> = { key: "age", value: 25 };
const pair2: Pair<number, boolean> = { key: 1, value: true };

이처럼 제네릭 인터페이스를 사용하면 다양한 타입 조합에 대해 재사용 가능한 타입 정의를 만들 수 있다!

 

여기서!

만약 pari1을 { key: 123, value: 25}로 할당하려고하면 key가 string이 아니기에

Type 'number' is not assignable to type 'string'

이런 오류가 발생할 것이다!


제네릭 클래스

클래스에서도 제네릭을 사용할 수 있다.

제네릭 클래스는 다양한 타입에 대해 재사용 가능한 컴포넌트를 만들 때 유용하다.

class Queue<T> {
    private items: T[] = [];
    
    enqueue(item: T): void {
        this.items.push(item);
    }
    
    dequeue(): T | undefined {
        return this.items.shift();
    }
    
    peek(): T | undefined {
        return this.items[0];
    }
    
    size(): number {
        return this.items.length;
    }
}

// 사용 예시
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
const firstItem = numberQueue.dequeue(); // 타입은 number | undefined

const stringQueue = new Queue<string>();
stringQueue.enqueue("Hello");
stringQueue.enqueue("World");
const peekedItem = stringQueue.peek(); // 타입은 string | undefined

위 코드는 제네릭을 사용한 간단한 큐 구현이다. (Queue 클래스는 어떤 타입의 아이템이든 저장할 수 있음)

클래스를 인스턴스화할 때 원하는 타입을 지정할 수 있으며, 그 후에는 지정된 타입만 큐에 추가할 수 있다!

이렇게 제네릭 클래스를 사용하면 타입 안전성을 유지하면서도 다양한 타입에 대해 동작하는 재사용 가능한 자료구조를 구현할 수 있다.


제네릭 제약조건

때로는 제네릭 타입에 특정 조건을 부여하고 싶을 때가 있다.

이럴 때 제네릭 제약조건(Generic Constraints)을 사용할 수 있다.

제약조건은 extends 키워드를 사용하여 표현한다.

interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// 사용 예시
logLength("Hello"); // 문자열은 length 속성이 있음
logLength([1, 2, 3]); // 배열도 length 속성이 있음
logLength({ length: 10, value: 3 }); // length 속성이 있는 객체

// 오류 예시
// logLength(10); // 오류: number 타입에는 length 속성이 없음

위 코드에서 T extends HasLength는 "T는 HasLength 인터페이스를 확장(구현)해야 한다"라는 의미다.

즉, T 타입은 반드시 length 속성을 가져야 한다.

따라서 문자열, 배열, length 속성을 가진 객체 등은 이 함수의 인자로 사용할 수 있지만, length 속성이 없는 숫자 등은 사용할 수 없다.

 

이렇게 제약조건을 사용하면 제네릭의 유연성을 유지하면서도 특정 조건을 만족하는 타입만 사용하도록 제한할 수 있다!!

 

keyof 연산자와 함께 사용하는 예제도 살펴보자.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person = {
    name: "John",
    age: 30,
    isEmployed: true
};

// 사용 예시
const name = getProperty(person, "name"); // 타입은 string
const age = getProperty(person, "age"); // 타입은 number
const isEmployed = getProperty(person, "isEmployed"); // 타입은 boolean

// 오류 예시
// const height = getProperty(person, "height"); // 오류: 'height'는 'person'의 키가 아님

이 예제에서 K extends keyof T는 "K는 T 타입의 키 중 하나여야 한다"라는 의미다.

즉, getProperty 함수는 객체 obj의 키로 존재하는 프로퍼티만 접근할 수 있도록 제한된다.

이렇게 하면 존재하지 않는 프로퍼티에 접근하는 오류를 컴파일 시점에 방지할 수 있다.

 

개인적으로 타입스크립트를 더 깊이 있게 사용하고 싶다면 제네릭을 마스터하는 것이 큰 도움이 될거라고 생각이 든다!

이 글이 제네릭 이해에 조금이나마 도움이 되었기를 바란다.

728x90

'TypeScript' 카테고리의 다른 글

[TypeScript] 타입 별칭(Type Aliases) 개념정리  (0) 2025.03.07
[TypeScript] 열거형(Enums) 개념정리  (0) 2025.03.05
[TypeScript] 인터페이스 개념정리  (0) 2025.03.03
[TypeScript] JavaScript의 진화!!!  (0) 2025.03.01
'TypeScript' 카테고리의 다른 글
  • [TypeScript] 타입 별칭(Type Aliases) 개념정리
  • [TypeScript] 열거형(Enums) 개념정리
  • [TypeScript] 인터페이스 개념정리
  • [TypeScript] JavaScript의 진화!!!
프론트 개발자 김현중
프론트 개발자 김현중
👋반갑습니다 저는 나눔을 실천하는 개발자 꿈나무 김현중입니다⌨️🚀
  • 프론트 개발자 김현중
    삥구의 개발블로그
    프론트 개발자 김현중
  • 전체
    오늘
    어제
    • 분류 전체보기 (92)
      • 알고리즘 (5)
      • Swift (3)
      • 컴퓨터네트워크 (1)
      • React (38)
      • Docker (1)
      • SQL (8)
      • Database (2)
      • 배포 (1)
      • Spring (9)
      • TypeScript (5)
      • Next.js (12)
      • Git (1)
      • 회고 (1)
      • 컴퓨터그래픽스 (2)
      • Python (1)
      • Brew (1)
      • LangChain (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    MySQL
    프론트엔드
    appRouter
    frontend
    nextjs
    Backend
    java
    ReactHooks
    백준
    Next.js
    react
    typescript
    database
    코딩테스트
    springboot
    javascript
    Spring
    데이터베이스
    웹개발
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.1
프론트 개발자 김현중
[TypeScript] 제네릭(Generics) 개념정리
상단으로

티스토리툴바