logoRawon_Log
홈블로그소개

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

Typescript

Typescript Generic

Rawon
2025년 11월 27일
목차
🎯 제네릭
제네릭이 필요한 상황
제네릭 함수로 해결하기
🎯 타입 변수 응용
예시 1) 여러 개의 타입 변수 사용
예시 2) 배열 타입 처리
예시 3) 튜플과 나머지 파라미터 활용
예시 4) 타입 변수 제한
🎯 map, forEach 메서드 타입 정의
map 메서드 타입 정의
forEach 메서드 타입 정의
🎯 제네릭 인터페이스
인덱스 시그니처와 함께 사용
🎯 제네릭 타입 별칭
🎯 제네릭 클래스
🎯 Promise 제네릭 타입
Promise 반환 함수 타입 정의

목차

🎯 제네릭
제네릭이 필요한 상황
제네릭 함수로 해결하기
🎯 타입 변수 응용
예시 1) 여러 개의 타입 변수 사용
예시 2) 배열 타입 처리
예시 3) 튜플과 나머지 파라미터 활용
예시 4) 타입 변수 제한
🎯 map, forEach 메서드 타입 정의
map 메서드 타입 정의
forEach 메서드 타입 정의
🎯 제네릭 인터페이스
인덱스 시그니처와 함께 사용
🎯 제네릭 타입 별칭
🎯 제네릭 클래스
🎯 Promise 제네릭 타입
Promise 반환 함수 타입 정의

이 글은 아래 강의를 바탕으로 정리한 글입니다. 🤗

plain
https://inf.run/UGoRu

🎯 제네릭

📚 generic이라는 단어를 직역하면 "일반적인", "포괄적인" 이라는 뜻을 가지고 있습니다.
그렇다면 제네릭 함수는 "포괄적인 함수" 라는 의미로 볼 수 있습니다.

그런 의미에서 제네릭 함수는 모든 타입에 두루두루 사용할 수 있는 함수, 즉 원하는대로 함수의 인수에 따라 반환값의 타입을 가변적으로 정해줄 수 있는 함수 입니다.

💡 그래서 제네릭이란? 함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동작하도록 만들어주는 TS의 놀라운 기능 중 하나 입니다.

제네릭이 필요한 상황

다음과 같이 다양한 타입의 매개변수를 받고 해당 매개변수를 그대로 반환하는 함수가 하나 있다고 가정하겠습니다.

typescript
function func(value: any){
  return value;
}

let num = func(10)
let str = func("string")

num.toUpperCase() // number 타입에 string 메서드를 적용해도 에러가 발생하지 않음 (런타임에서 오류)

이 함수는 다양한 타입의 매개변수를 받아야 하기 때문에 매개변수 value의 타입을 any로 해두었습니다.

그럼으로서 func 함수의 반환값 타입이 return 문을 기준으로 추론되어 num과 str 타입은 any 타입이 됩니다.

⚠️ 그렇기 때문에 위와 같이 number 타입의 변수에 string 타입의 메서드를 사용해도 TS는 오류를 감지하지 못하고 결국 런타임에서 오류가 발생할 것 입니다.

만약 any가 아니라 unknown으로 타입을 수정한다면 위 string 메서드를 사용하려 할때, TS는 에러를 알려줄 것 입니다.

그러나 toFixed 같은 Number 타입의 메서드 호출도 함께 오류로 판단하게 됩니다.

따라서 num 변수에는 10이 저장될 것이 분명한데도 불구하고 비효율적으로 타입 좁히기를 이용해야 합니다.

단지, 매개변수 타입을 그대로 반환하는 함수를 만들고 싶었는데, 모든 타입에 대한 조건을 만들어주기는 매우 번거로울 것 같습니다.

제네릭 함수로 해결하기

이럴 때, 다음과 같이 func 함수를 제네릭 함수로 만들면 아주 간단하게 해결이 가능합니다.

제네릭 함수로 만드는 방법은 아래와 같이 함수 이름 뒤에 꼽쉐를 열고 타입을 담는 변수인 타입 변수 T를 선언하고 매개변수와 반환값의 타입을 이 타입변수 T로 설정합니다.

typescript
function func<T>(value: T): T {
  return value;
}

let num = func(10); // num은 number 타입
let str = func("hello"); // str은 string 타입
let bool = func(true); // bool은 boolean 타입

let arr = func<[number, number, number]>([1, 2, 3]); // arr은 튜플 타입
// <[number, number, number]> 와 같이 써주면 이 타입이 <T>에 담기게 됨.
// 즉, 매개변수 타입 T도 튜플, 반환값도 튜플

⭐ 타입 변수 선언 <T>

자바스크립트 변수처럼 타입을 담는 변수인 T를 타입 변수라 하며,

이 타입변수에 어떤 타입이 담기는지는 함수를 호출할 때마다 결정됩니다.

(value: T) → 매개변수 타입

(): T → 반환값 타입

image.png


🎯 타입 변수 응용

예시 1) 여러 개의 타입 변수 사용

만약 2개의 타입 변수가 필요한 상황이라면 다음과 같이 T와 U처럼 2개의 타입 변수를 사용해도 됩니다.

typescript
function swap<T, U>(a: T, b: U) {
  return [b, a];
}

const [a, b] = swap("1", 2);

예시 2) 배열 타입 처리

다양한 배열 타입을 인수로 받는 제네릭 함수를 만들어야 한다면 다음과 같이 할 수 있습니다.

typescript
function returnFirstValue<T>(data: T[]){
  return data[0]
}

let num1 = returnFirstValue([0, 1, 2]);  // number

let str1 = returnFirstValue(["hello", "world"]);  // string

let str1 = returnFirstValue([1, "hello", "world"]);  // number | string

예시 3) 튜플과 나머지 파라미터 활용

예시 2번에서 만약 반환값의 타입을 첫번째 요소의 타입이 되도록 하려면 다음과 같이 튜플 타입과 나머지 파라미터를 이용하면 됩니다.

typescript
function returnFirstValue<T>(data: [T, ...unknown[]]) {
  return data[0];
}

let str = returnFirstValue([1, "hello", "mynameis"]);
// number

함수 매개변수의 타입을 정의할 때, 튜플 타입을 이용해 첫번째 요소의 타입은 T 그리고 나머지 요소의 타입은 ...unknown[] 으로 길이도 타입도 상관 없도록 정의합니다.

예시 4) 타입 변수 제한

📌 타입 변수 제한이란 함수를 호출하고 인수로 전달할 수 있는 값의 범위에 제한을 두는 것을 의미합니다.

이때, 타입 변수를 제한할 때는 extends 키워드를 사용합니다.

typescript
function getLength<T extends { length: number }>(data: T) {
  return data.length;
}

  getLength("123");            // ✅

  getLength([1, 2, 3]);        // ✅

  getLength({ length: 1 });    // ✅

  getLength(undefined);        // ❌

  getLength(null);             // ❌

위와 같이 T extends { length: number } 라고 정의하면 T는 이제 {length: number} 객체 타입의 서브 타입이 됩니다.

즉, 이제 T는 무조건 Number 타입의 프로퍼티인 length를 가지고 있는 타입이 되어야 한다는 것 입니다.

✅ 따라서 이렇게 extends를 이용해 타입변수를 제한하면 아래와 같은 결과가 나타납니다.

  • 1번 호출은 인수로 length 프로퍼티가 존재하는 String 타입의 값을 전달 했으므로 허용됩니다.

  • 2번 호출은 인수로 length 프로퍼티가 존재하는 Number[] 타입의 값을 전달 했으므로 허용됩니다.

  • 3번 호출은 인수로 length 프로퍼티가 존재하는 객체 타입의 값을 전달 했으므로 허용됩니다.

  • 4번 호출은 인수로 undefined을 전달했으므로 오류가 발생합니다.

  • 5번 호출은 인수로 null을 전달했으므로 오류가 발생합니다.


🎯 map, forEach 메서드 타입 정의

map 메서드 타입 정의

JS의 배열 메서드인 map은 원본 배열의 각 요소에 콜백함수를 수행하고 반환된 값들을 모아 새로운 배열로 만들어 반환합니다.

typescript
const arr1 = [1, 2, 3];
const newArr = 
arr1.map
((item) => item * 2); // [2, 4, 6]

이때, map 메서드 콜백함수의 매개변수 타입이 자동으로 추론되는 이유는 map 메서드의 타입이 어딜가에 별도로 선언되어 있기 때문입니다. (lib.es5.d.ts)

실제로 map 메서드와 같은 역할을 하는 함수를 직접 만들고 타입도 정의하면 다음과 같습니다.

typescript
function map(arr: unknown[], callback: (item: unknown) => unknown): unknown[] {}

메서드를 적용할 배열을 매개변수 arr로 받고 콜백함수를 매개변수 callback으로 받습니다.

또한, map 메서드는 모든 타입의 배열에 적용할 수 있기 때문에 arr 타입은 unknown[]으로 정의하며, callback의 타입은 배열 요소 하나를 매개변수로 받아 특정 값을 반환하는 함수로 정의합니다.

이제 여기에 타입 변수를 추가하여 제네릭 함수로 만들면 아래와 같습니다.

typescript
function map<T>(arr: T[], callback: (item: T)=> T): T[] {
  let result = [];
  for (let i; i < arr.length; i++){
    result.push(callback(arr[i]))
  }
  return result
}

⚠️ 그러나 위 코드에는 한가지 문제가 있습니다.

바로 map 메서드는 원본 배열 타입과 다른 타입의 배열로도 변환할 수 있어야 하는데, 지금 정의된대로 한다면 매개변수 타입이 특정 타입으로 정해지면 그 타입으로 반환되어야 한다는 것 입니다.

이를 해결하기 위해 아래와 같이 타입 변수를 하나 더 추가해줍니다.

typescript
function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
  let result: U[] = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i]));
  }
  return result;
}

map(arr, (it) => it.toString());
// string[] 타입의 배열을 반환
// 결과 : ["1", "2", "3"]

forEach 메서드 타입 정의

forEach도 위 map 메서드와 같은 방식으로 직접 구현하면 됩니다.

typescript
function forEach<T>(arr: T[], callback: (item: T) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}

forEach(arr2, (item) => console.log(item.toFixed()));
forEach(["hi", "hello"], (item) => console.log(item.toUpperCase()));

🎯 제네릭 인터페이스

제네릭은 함수 뿐만 아니라 인터페이스에도 적용할 수 있습니다.

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

// let keyPair: KeyPair = {};  // 'KeyPair<K, V>' 제네릭 형식에 2 형식 인수가 필요합니다.

let keyPair: KeyPair<string, number> = {
  key: "key",
  value: 0,
};

let keyPair2: KeyPair<boolean, string[]> = {
  key: true,
  value: ["1"],
};

변수 keyPair의 타입으로 KeyPair<string, number>를 정의했습니다.

그 결과 K에는 string이 V에는 number가 각각 할당되어 key 프로퍼티는 string타입, value 프로퍼티는 number 타입인 객체 타입이 됩니다. 따라서 값으로 해당 타입의 객체를 저장합니다.

⭐ 제네릭 인터페이스는 제네릭 함수와는 달리 타입으로 정의할 때, 반드시 <> 로 타입변수의 타입을 직접 지정해주어야 합니다.

인덱스 시그니처와 함께 사용

제네릭 인터페이스는 인덱스 시그니처와 함께 사용하면 다음과 같이 기존보다 훬씬 더 유연한 객체 타입을 정의할 수 있습니다.

typescript
// 제네릭 적용 전
interface NumberMap {
  [key: string]: number;
}

let numberMap1: NumberMap = {
  key: -11,
  key2: 2,
  // 3: "" // 오류 발생
};

// 제네릭 인터페이스를 결합함으로서 value의 타입을 마음대로 바꿔쓸 수 있는
// 인덱스 시그니쳐 타입을 만들 수 있음.
interface StringMap<V> {
  [key: string]: V;
}

let stringMap1: StringMap<string> = {
  key: "value",
};

let stringMap2: StringMap<number> = {
  key: 1,
  key2: 4,
};

let stringMap3: StringMap<boolean> = {
  key: true,
  key2: false,
};

🎯 제네릭 타입 별칭

제네릭 인터페이스를 만드는 방법과 거의 비슷합니다.

typescript
type Map2<V> = {
  [key: string]: V;
};

let stringMap4: Map2<string> = {
  key: "value",
};

let stringMap5: Map2<number> = {
  key: 1,
  key2: 4,
};

🎯 제네릭 클래스

typescript
class List<T> {
  constructor(private list: T[]) {}
  push(data: T) {
    this.list.push(data);
  }
  pop() {
    return this.list.pop();
  }
  print() {
    console.log(this.list);
  }
}

const numberList = new List([1, 2, 3]);
numberList.pop(); // 3이 제거
numberList.push(4); // 4 추가
numberList.print(); // 1, 2, 4

const stringList = new List(["hello", "world"]);
stringList.pop(); // "world" 제거
stringList.push("typescript"); // "typescript" 추가
stringList.print(); // "hello", "typescript"

🎯 Promise 제네릭 타입

📌 Promise는 resolver나 reject를 호출해서 전달하는 비동기 작업 결과값의 타입을 자동으로 추론하지 못합니다.

그래서 기본적으로 unknown 타입으로 저장됩니다.

그러나 여기에 일일이 타입 좁히기를 적용하기엔 복잡하기 때문에 제네릭 타입을 사용하여 타입을 지정할 수 있습니다.

이미 JS 내장 클래스인 Promise는 TS에서 제네릭 클래스로 구현되어 있습니다.

그러므로 Promise 의 생성자를 호출할 때, 다음과 같이 타입 변수에 할당할 타입을 직접 설정해주면 해당 타입이 바로 resolver 결과값의 타입이 됩니다.

typescript
const promise = new Promise<number>((resolve, reject) => {
  setTimeout(() => {
    // 결과값 : 20
    resolve(20);
  }, 3000);
});

promise.then((response) => {
  // response는 number 타입
  console.log(response);
});

promise.catch((error) => {
  if (typeof error === "string") {
    console.log(error);
  }
});

Promise 반환 함수 타입 정의

서버로부터 게시물 데이터를 조회하는 함수와 같이 어떤 함수가 Promise 객체를 반환한다면 함수의 반환값 타입을 위해 다음과 같이 할 수 있습니다.

typescript
interface Post {
  id: number;
  title: string;
  content: string;
}

// fetchPost 함수의 반환값을 Promise<Post> 타입으로 정의할 수도 있고 (권장!)
// Promise<Post> 타입의 결과값을 반환할거라고 타입변수를 할당해 줌수도 있음. new Promise<Post>((resolver, reject) => {...
function fetchPost(): Promise<Post> {
  return new Promise((resolver, reject) => {
    setTimeout(() => {
      resolver({
        id: 1,
        title: "Hello World",
        content: "This is a test post",
      });
    }, 3000);
  });
}

const postRequest = fetchPost();

postRequest.then((res) => {
  console.log(
res.id
);
  console.log(res.title);
  console.log(res.content);
});