[TypeScript] 타입스크립트의 제네릭(Generic)타입 알아보기

2025. 1. 1. 23:37·TypeScript

 

 

 

 

 


 

 

 

 

제네릭 타입이란?

제네릭 타입은 특정 데이터 타입에 국한되지 않는 유연한 코드를 작성할 수 있도록 도와주는 도구입니다. 함수, 클래스, 인터페이스 등에 제네릭을 사용하면 호출 시점에 타입을 지정할 수 있어 유연성과 타입 안전성, 재사용성을 동시에 얻을 수 있는 강력한 기능입니다.

 

 

 

 

제네릭 타입 사용하기

 

 

 

제네릭의 기본 문법

제네릭은 함수나 클래스에서 타입을 매개변수처럼 취급합니다. 제네릭 타입은 자유롭게 작명이 가능하지만 일반적으로 Type의 약자인 T를 사용하며 다음 타입은 U, V 등을 사용합니다.

function merge<T, U>(objA: T, objB: U) {
  return Object.assign({}, objA, objB);
  // Object.assign(target, ...sources) 객체를 합쳐주는 메서드
}

const mergedObj = merge({ name: '빵빵이' }, { age: 25 });

console.log(mergedObj); // { name: '빵빵이', age: 25 }
console.log(mergedObj.age); // ✅ 정상적으로 25 출력

 

 

아래 코드와 비교하면 매개변수와 반환 값의 타입을 어떻게 설정하느냐에 따라 코드의 유연성과 타입 안전성에 차이가 나는 것을 볼 수 있습니다.

function merge(objA: object, objB: object) {
	return Object.assign({}, objA, objB)
}

const mergedObj = merge({name: '빵빵이'}, {age: 25});

console.log(mergedObj); // {name: '빵빵이', age: 25}
mergedObj.age; // ❌ 에러 발생: 'age' 속성에 접근할 수 없음!

 

objA와 objB는 타입이 object로 선언되었습니다. object는 객체임을 보장하지만 내부 구조에 대한 정보는 알 수 없습니다. 2번째 코드의 경우 반환 값의 타입도 object로 추론되기 때문에 age와 같은 속성에 접근할 수 없습니다. 

 

하지만 제네릭을 사용하면 merge함수가 호출될 때 매개변수의 구체적인 타입을 자동으로 추론합니다. T와 U는 각각

{name: string}과 {age: number}로 추론되며 반환 타입은 T & U로 합쳐집니다. 그로 인해 타입스크립트는 정확한 타입을 알고 속성에 더 안전하게 접근할 수 있습니다.

 

 

 

 

제약 조건

제네릭 타입은 어떤 타입이라도 허용하지만, 특정 조건을 추가할 수도 있습니다. 우선 첫 번째 예제 코드를 보겠습니다.

function merge<T, U>(objA: T, objB: U) {
  return Object.assign({}, objA, objB);
}
const mergedObj = merge({name: '김옥지', hobbies: ['sports']}, 30);

console.log(mergedObj); // {name: '김옥지', hobbies: Array(1)}

 

Object.assign() 메서드는 객체를 합쳐주는 메서드입니다. 우리는 T와 U는 어떤 타입이던 상관이 없지만 객체이기는 해야 하는 상황입니다. 하지만 merge의 두 번째 인자에 number 타입의 30을 입력하고 컴파일을 한다면 그대로 실행됩니다. (오류가 잡히지 않음) 이럴 때 필요한 게 제약 조건입니다.

function merge<T, U extends object>(objA: T , objB: U) {
  return Object.assign({}, objA, objB);
}
const mergedObj = merge({name: '김옥지', hobbies: ['sports']}, 30); // Argument of type 'number' is not assignable to parameter of type 'object'.

console.log(mergedObj);

 

 

Keyof로 제약 조건 추가

 

function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U) {
  return `Value: ${obj[key]}`;
}

console.log(extractAndConvert({ name: '옥지' }, 'name')); // Value: 옥지
// console.log(extractAndConvert({ name: '옥지' }, 'age')); // 오류 발생

 

 

 

 

 

제네릭 클래스

제네릭을 클래스에 적용하면 특정 데이터 타입만 처리하도록 제한할 수 있습니다.

class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

// 사용 예시
const textStorage = new DataStorage<string>();
textStorage.addItem('빵빵이');
textStorage.addItem('옥지');
textStorage.removeItem('빵빵이');
console.log(textStorage.getItems()); // ['옥지']

 

 

 

 

 

제네릭의 참조 타입 문제

제네릭을 사용할 때 참조 타입의 문제가 있습니다.

class DataStorage<T> {
	private data: T[] = [];

	addItem(item: T) {
		this.data.push(item);
	}

	removeItem(item: T) {
		this.data.splice(this.data.indexOf(item), 1);
	}

	getItem() {
		return [...this.data];
	}
}

const objStorage = new DataStorage<object>();
objStorage.addItem({name: '빵빵이'}); 
objStorage.addItem({name: '김옥지'});
objStorage.removeItem({name: '빵빵이'});
console.log(objStorage.getItem()); // 0 : {name: '빵빵이'}

 

위 코드를 보면 정상적으로 보일 수 있지만 정상적으로 작동되는 코드가 아닙니다. 우리는 '빵빵이'를 지우고 싶은데 이상하게 '빵빵이'가 지워지지 않고 마지막 요소인 '김옥지'가 지워지는 모습입니다. 그 이유는 우리가 객체를 다루고 있기 때문입니다. 객체는 참조 타입으로 원시값을 다루는 로직은 비 원시 값(객체, 배열 등)을 제대로 다루지 못합니다. 위 코드에서 우리가 추가한 '빵빵이'와 삭제하려는 '빵빵이'는 값은 같아 보이지만 메모리에서는 완전히 새로운 객체이고 다른 주소를 가집니다. 그로 인해서 indexOf에 객체를 전달하여 원하는 값을 찾기 못해 -1을 반환, 따라서 마지막 요소를 제거합니다.

 

 

해결방법 1. 추가 제거하려는 값을 변수에 담아 사용하기.

const objStorage = new DataStorage<object>();
const nameObj = {name: '빵빵이'}
objStorage.addItem(nameObj); //nameObj 같은 메모리 주소 사용용
objStorage.addItem({name: '김옥지'});
objStorage.removeItem(nameObj); // nameObj 같은 메모리 주소 사용
console.log(objStorage.getItem()); // 0 : {name: '김옥지'}

 

 

해결방법 2. 타입을 원시 값으로만 작동하게 하기.

class DataStorage<T extends string | number | boolean> {
	private data: T[] = [];

	addItem(item: T) {
		this.data.push(item);
	}

	removeItem(item: T) {
		if(this.data.indexOf(item) === -1) {
			return;
		}
		this.data.splice(this.data.indexOf(item), 1);
	}

	getItem() {
		return [...this.data];
	}
}
const nameStorage = new DataStorage<string>();
nameStorage.addItem('빵빵이');
nameStorage.addItem('김옥지');
nameStorage.removeItem('빵빵이');

console.log(nameStorage.getItem()); // ['김옥지']

 

 

 

 

 

 

 

제네릭 타입과 유니온 타입의 차이점은?

제네릭 타입 

  • 타입을 동적으로 설정
    • 사용자가 호출 시 원하는 타입을 설정하거나, 타입스크립트가 자동으로 추론합니다.
  • 타입 안정성 보장
    • 제네릭을 활용하면 반환값과 매개변수 간의 타입 관계를 유지할 수 있습니다.
  • 다양한 타입을 다룰 수 있지만, 타입 정보는 유지되어 유연성과 안정성이 보장됩니다.

 

유니온 타입

 

  • 작성한 여러 타입을 모두 허용합니다. 
  • 특정 타입이 무엇인지 정확히 알기 위해 타입 가드를 사용해야 할 때가 많습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

'TypeScript' 카테고리의 다른 글

[TypeScript] 타입스크립트의 인터페이스(interface) 알아보기  (0) 2024.12.30
[TypeScript] 타입스크립트의 기본 타입  (0) 2024.12.26
'TypeScript' 카테고리의 다른 글
  • [TypeScript] 타입스크립트의 인터페이스(interface) 알아보기
  • [TypeScript] 타입스크립트의 기본 타입
끄적코딩
끄적코딩
매일 조금씩 끄적이는 중
  • 끄적코딩
    끄적코딩일지
    끄적코딩
    • 분류 전체보기 (30)
      • HTML & CSS (2)
      • 자료구조 & 알고리즘 (7)
      • JavaScript (9)
      • CS (2)
      • React.js (2)
      • Project (3)
      • TypeScript (3)
      • 코딩테스트 (2)
  • hELLO· Designed By정상우.v4.10.3
끄적코딩
[TypeScript] 타입스크립트의 제네릭(Generic)타입 알아보기
상단으로

티스토리툴바