Generics

2개의 게시글

TypeScript

[TypeScript] 제네릭(Generic) 응용

November 29, 2025

지난 포스팅에서 제네릭의 기본 개념(T를 변수처럼 사용)을 익혔다. 이번에는 다양한 상황에서 제네릭을 어떻게 응용할 수 있는지 알아본다. 1. 제네릭 함수 응용 1-1. 다중 타입 변수 (T, U) 필요하다면 타입 변수를 2개 이상 선언해서 사용할 수 있다. TypeScript function swap<T, U>(a: T, b: U) { return [b, a]; } const [a, b] = swap("1", 2); // a는 number, b는 string 타입으로 자동 추론됨 1-2. 배열 타입 다루기 (T[]) 배열의 요소 타입을 제네릭으로 받으면, 다양한 타입의 배열을 처리하는 함수를 만들 수 있다. TypeScript function returnFirstValue<T>(data: T[]) { return data[0]; } let num = returnFirstValue([0, 1, 2]); // T는 number, 반환값 number let str = returnFirstValue([1, "hello", "mynameis"]); // T는 number | string 💡 튜플로 첫 번째 요소 타입 확정하기 만약 배열의 첫 번째 요소 타입을 정확히 추론하고 싶다면 튜플과 나머지 파라미터를 활용한다. TypeScript function returnFirstValue<T>(data: [T, ...unknown[]]) { return data[0]; } let str = returnFirstValue([1, "hello", "mynameis"]); // 반환값이 정확히 number로 추론됨 (유니온 타입 아님) 1-3. 타입 변수 제한하기 (extends) 타입 변수에 아무 타입이나 들어오는 것을 막고, 특정 조건을 만족하는 타입만 받도록 제한할 수 있다. TypeScript // length 프로퍼티가 있는 타입만 허용 function getLength<T extends { length: number }>(data: T) { return data.length; } getLength("123"); // ✅ (string은 length 있음) getLength([1, 2, 3]); // ✅ (array는 length 있음) getLength({ length: 1 }); // ✅ (객체에 length 있음) // getLength(undefined); // ❌ (length 없음) --- 2. map & forEach 메서드 직접 구현하기 자바스크립트의 내장 메서드인 map과 forEach도 제네릭을 이용해 타입을 정의할 수 있다. map 메서드 타입 정의 map은 원본 배열의 타입(T)과 변환된 배열의 타입(U)이 다를 수 있으므로, 두 개의 타입 변수가 필요하다. TypeScript function map<T, U>(arr: T[], callback: (item: T) => U): U[] { let result = []; for (let i = 0; i < arr.length; i++) { result.push(callback(arr[i])); } return result; } const arr = [1, 2, 3]; const result = map(arr, (it) => it.toString()); // result는 string[] 타입 ["1", "2", "3"] forEach 메서드 타입 정의 forEach는 반환값이 없으므로(void), 비교적 간단하게 정의할 수 있다. TypeScript function forEach<T>(arr: T[], callback: (item: T) => void) { for (let i = 0; i < arr.length; i++) { callback(arr[i]); } } --- 3. 제네릭 인터페이스 & 타입 별칭 3-1. 제네릭 인터페이스 인터페이스 이름 뒤에 <T> 등을 붙여 정의한다. TypeScript interface KeyPair<K, V> { key: K; value: V; } // ⚠️ 주의: 변수 정의 시 반드시 타입을 명시해야 함 let keyPair: KeyPair<string, number> = { key: "key", value: 0, }; 3-2. 인덱스 시그니처와 함께 사용 TypeScript interface Map<V> { [key: string]: V; } let booleanMap: Map<boolean> = { key: true, }; 3-3. 제네릭 타입 별칭 인터페이스와 동일하게 사용 가능하다. TypeScript type Map2<V> = { [key: string]: V; }; --- 4. 실전 예제: 제네릭 인터페이스 활용 유저의 프로필(profile)이 상황에 따라 Student일 수도 있고 Developer일 수도 있다면? 제네릭을 이용해 중복 코드를 없애고 타입 안정성을 높일 수 있다. TypeScript interface Student { type: "student"; school: string; } interface Developer { type: "developer"; skill: string; } // 제네릭 인터페이스 정의 interface User<T> { name: string; profile: T; } // 학생만 이용 가능한 함수: 매개변수 타입을 User<Student>로 제한 function goToSchool(user: User<Student>) { const school = user.profile.school; // 타입 좁히기 없이 바로 접근 가능! console.log(${school}로 등교 완료); } 함수 내부에서 if문으로 타입을 좁힐 필요가 없어져 코드가 훨씬 깔끔해진다. --- 5. 제네릭 클래스 클래스도 제네릭을 사용하면 하나의 클래스로 다양한 타입의 데이터를 처리할 수 있다. TypeScript class List<T> { constructor(private list: T[]) {} push(data: T) { this.list.push(data); } print() { console.log(this.list); } } // 생성자 인수로 타입 추론 가능 const numberList = new List([1, 2, 3]); // T는 number const stringList = new List(["a", "b"]); // T는 string 제네릭이 없다면 NumberList, StringList 클래스를 따로 만들어야 했을 것이다. --- 6. 프로미스(Promise)와 제네릭 비동기 처리에 사용되는 Promise는 제네릭 클래스로 구현되어 있다. 성공했을 때(resolve) 반환되는 값의 타입을 제네릭으로 지정해 줄 수 있다. TypeScript // 1. 생성 시점에 타입 지정 const promise = new Promise<number>((resolve, reject) => { setTimeout(() => { resolve(20); }, 3000); }); promise.then((response) => { // response는 number 타입 (20) console.log(response); }); 함수의 반환값으로 사용할 때는 다음과 같이 명시하는 것이 직관적이다. TypeScript interface Post { id: number; title: string; content: string; } // 반환값 타입에 Promise<Post> 명시 function fetchPost(): Promise<Post> { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ id: 1, title: "게시글 제목", content: "게시글 본문", }); }, 3000); }); } > 참고: reject로 전달되는 에러 값의 타입은 정의할 수 없으며, 기본적으로 unknown (또는 any)으로 처리된다. > --- 요약 1. 함수 응용: 다중 타입 변수, 배열 처리, extends를 이용한 타입 제한 등 다양하게 활용 가능. 2. 인터페이스/타입 별칭: 객체의 프로퍼티 타입을 유연하게 변경하며 재사용성 극대화. 3. 클래스: 하나의 클래스로 여러 타입의 데이터를 처리하는 범용 클래스 생성. 4. Promise: 비동기 작업의 결과값 타입을 제네릭으로 정의하여 안전하게 처리.

TypeScriptStudyGenericsPromise
TypeScript

[TypeScript] 제네릭(Generic)

November 29, 2025

타입스크립트를 공부하다 보면 함수나 인터페이스 뒤에 붙은 <T> 같은 기호를 본 적이 있을 것이다. 이것이 바로 제네릭(Generic) 이다. 제네릭은 단일 타입이 아닌 다양한 타입에서 동작하는 컴포넌트를 작성할 수 있게 해주는 타입스크립트의 놀라운 기능이다. 쉽게 말해 타입을 변수처럼 유연하게 다루는 도구라고 볼 수 있다. 1. 제네릭이 필요한 이유 제네릭을 왜 써야 하는지 이해하기 위해, 인수로 받은 값을 그대로 반환하는 간단한 함수 func를 만들어보자. 시도 1: any 타입 사용하기 다양한 타입을 받아야 하니, 일단 매개변수 타입을 any로 설정해 보았다. TypeScript function func(value: any) { return value; } let num = func(10); let str = func("string"); 이 코드는 잘 동작하는 것처럼 보이지만 치명적인 단점이 있다. 반환값의 타입마저 any가 되어버린다는 점이다. TypeScript num.toUpperCase(); // ❌ 런타임 오류 발생! num에는 분명 숫자 10이 들어있지만, 타입스크립트는 이를 any로 인식하기 때문에 문자열 메서드인 .toUpperCase()를 써도 에러를 잡아내지 못한다. 결국 실행 시점에 프로그램이 뻗어버리는 위험한 상태가 된다. 시도 2: unknown 타입 사용하기 그렇다면 any보다 안전한 unknown을 쓰면 어떨까? TypeScript function func(value: unknown) { return value; } let num = func(10); // num의 타입은 unknown // num.toFixed(); // ❌ 오류 발생 (unknown에는 메서드 사용 불가) unknown은 안전하지만 너무 엄격하다. 값을 사용하려면 매번 typeof 등을 이용해 타입 좁히기를 해야 한다. TypeScript if (typeof num === "number") { num.toFixed(); // 귀찮은 과정이 필요함 } 우리가 원하는 건 단순하다. "숫자를 넣으면 숫자가 반환되고, 문자를 넣으면 문자가 반환되는" 그런 유연한 함수다. 이럴 때 제네릭이 필요하다. --- 2. 제네릭(Generic) 함수 제네릭(Generic) 이라는 단어는 '일반적인', '포괄적인'이라는 뜻을 가진다. 즉, 모든 타입의 값을 두루두루 포괄하여 적용할 수 있는 범용적인 함수를 만든다는 의미다. 제네릭 기본 문법 <T> 제네릭 함수는 함수 이름 뒤에 꺾쇠 < >를 열고 타입 변수를 선언하여 사용한다. 보통 T (Type의 약자)를 많이 쓴다. TypeScript // <T>: 타입 변수 선언 // (value: T): 매개변수 타입 // : T : 반환값 타입 function func<T>(value: T): T { return value; } 이제 이 함수를 호출할 때 전달하는 인수의 타입에 따라 T가 결정된다. TypeScript let num = func(10); // 1. 인수 10(number)이 전달됨 // 2. T가 number로 추론됨 // 3. 매개변수와 반환값 타입이 모두 number로 결정됨 // 결과: num은 number 타입 ✅ TypeScript let str = func("string"); // 1. 인수 "string"이 전달됨 // 2. T가 string으로 추론됨 // 결과: str은 string 타입 ✅ 이제 any처럼 타입 안정성을 잃지도 않고, unknown처럼 귀찮게 타입을 좁힐 필요도 없다. --- 3. 명시적으로 타입 지정하기 대부분의 경우 타입스크립트가 타입을 알아서 잘 추론해 주지만, 가끔은 우리가 직접 타입을 지정해야 할 때가 있다. 예를 들어 배열을 넣었을 때, 튜플 타입으로 추론되길 원한다면 다음과 같이 명시할 수 있다. TypeScript // 자동으로 추론시키면? let arr = func([1, 2, 3]); // T는 number[] 로 추론됨 // 명시적으로 지정하면? let tuple = func<[number, number, number]>([1, 2, 3]); // T는 [number, number, number] 튜플 타입으로 결정됨 함수 호출 시 <타입>을 직접 적어주면, 타입스크립트는 추론하지 않고 우리가 적어준 타입을 T에 할당한다. --- 요약 1. 문제점: any는 타입 검사를 포기하고, unknown은 사용하기 너무 번거롭다. 2. 해결책: 제네릭을 사용하면 입력값의 타입에 따라 반환값의 타입이 유연하게 결정된다. 3. 문법: 함수 이름 뒤에 <T>를 붙여 타입 변수를 선언하고 사용한다. 4. 사용: 일반적으로는 타입이 자동 추론되지만, 필요하다면 <Type>을 명시하여 호출할 수 있다.

TypeScriptStudyGenerics기초문법