본문 바로가기

Front-End/TypeScript

TypeScript #14 (제네릭)

반응형

 

타입을 일반화하여 코드 재사용성과 타입 안정성을 동시에 얻을 수 있게 해주는 도구

 

 

함수를 작성할 때 타입을 고정하지 않고, 나중에 호출하는 시점에 타입을 지정할 수 있도록 만든 타입 매개변수

 

T 타입의 placeholder (임시 변수)입니다. 이 함수는 string, number, boolean 등 어떤 타입이든 받을 수 있고, 그 타입 그대로 반환합니다.

 

 

제네릭을 쓰는 이유?

 

 

 

예문

function echo<T>(input: T): T {         // 함수에도 <T>, 입력값에도 T, 결과값에도 T
    console.log(typeof input, input);   // 출력값이랑 출력값의 타입을 알고 싶다
    return input;
}

echo<string>("Hello"); // string "Hello"
echo<number>(123);     // number 123

 

 

 

※ Identity 함수 

 

일반적으로 함수는 이렇게 쓰고 다니지만,

function identityNumber(value: number): number {
  return value;
}

 

Identity 함수에 제네릭을 선언해서 받으면, 결과값도 받을 당시의 타입 그대로 출력한다.

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

 

 

※ 이중 제네릭 - 객체 (K: 키 key, V: 값 value)

// 제네릭 타입을 받는 함수의 기본형부터 다시 보기
function '함수 이름'<제네릭 타입 선언>(받을 인자 선언): 결과값 선언 {
    return 출력할 무언가;
}

// 제네릭 타입, 받을 인자, 결과값 전부 제네릭이 들어가야 함.

 

function getValue<K extends string, V>(obj: Record<K, V>, key: K): V {
    return obj[key];
}

// 상위 객체 생성 (name: string, age: <T>)
const objects = { name: "john", age: 20 };

// 객체 생성 - getValue 함수에 obj인자(위에서 만든 객체인 objects)랑 key인자(객체의 속성) 넣음.
let result1 = getValue(objects, "name");
let result2 = getValue(objects, "age");

console.log(`${result1}, ${result2}세`);

 

 

함수 선언부분을 차근차근 뜯어보자.

  • 제네릭에서의 extends는 클래스 상속이 아니라 타입 제약으로 쓰임.
  • K는 string(혹은 string 하위, 문자열 리터럴 타입)으로 타입을 제한하고, V는 제한하지 않음.
  • Record<K, V>는 TypeScript의 유틸리티 타입 (= "키가 K 타입이고, 값이 V 타입인 객체")

 

※ Record<K, V> 가 뭔데?

// Record<K, V> 아래와 같은 역할을 한다.
type Record<K extends string | number | symbol, V> = {
  [P in K]: V;
}
// [K]배열 안에 P가 있는지 확인하는 연산자.

// 아래처럼 K, V 각각 타입을 지정하고, 그 위에 객체까지 만들어주는 기능이라고 보면 된다.
type K = string | number | symbol
type V = any
type MyRecord = { [P in K]: V }

//결과
{ [key: string]: valueType }

 

- 위에서 'obj: Record<K, V>'라고 했으니, 받을 인자 중 하나인 obj는 K, V 쌍으로 받으면서 객체를 만드는 타입이구나~

 

 

그럼 key: K는 뭐야?

- 받을 인자가 obj(: obj의 타입)랑 key라서 함수를 호출할 때 'getValue(obj, key)'의 key 타입으로 K를 받겠다는 말인데,

- 바로 앞에서 이해했듯 { K : V } 중에서 K를 넣어서 value값을 호출하겠다는 말인 것이다.

 

 

 

제네릭 인터페이스

// interface도 제네릭 타입으로 받을 수 있음.
interface Box<T> {
    value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };


// 키인 'value'에 직접 접근
console.log(`${stringBox.value}, No.${numberBox.value}`);

 

 

 

제네릭 클래스

// 클래스에도 제네릭 타입
class Stack<T> {
    private items: T[] = [];

    // push로 밀어 넣을 값의 타입도 제네릭
    push(item: T) {
        this.items.push(item);
    }

    // pop으로 꺼내 먹을 값의 타입도 제네릭, 없으면 undefined로 예외 처리
    pop(): T | undefined {
        return this.items.pop();
    }
}

// 객체 생성할 때 클래스<타입>을 써서 생성. <타입>은 뭐든 바꿀 수 있음.
const numberStack = new Stack<number>();

// 메서드를 실행할 때에도 위 클래스에서 정의했듯, ()안에 들어갈 값의 타입은 바꿀 수 있음.
// 하지만 바로 위에서 객체를 생성할 때의 타입과 같아야 함.
numberStack.push(1);
numberStack.push(2);

console.log(numberStack);

 

숫자로 했을 때,

 

문자열로 했을 때, (※ 객체 생성시 <number>를 <string>으로 변경)

 

 

 

 

실전 예제

// ApiResponse에 대해서 data 속성만 제네릭 타입으로 받는 interface 설정
interface ApiResponse<T> {
    status: number;
    message: string;
    data: T;
}

// userResponse라는 객체 생성 - 위 interface 에서 제네릭 타입으로 받은 걸
// 객체에서도 당연히 interface를 따를 거니까, stat이랑 mesg는 정해진 타입대로 받을거고, data도 받을건데,
// 위에서 제네릭 타입으로 설정했던 data 속성에서 
// name은 string타입으로, age는 number타입으로 해서 {키 : 밸류} 형태로 받겠다
const userResponse: ApiResponse<{ name: string; age: number }> = {
    status: 200,
    message: "Success",
    data: {
        name: "Alice",
        age: 25,
    },
};

console.log(userResponse);

 

 

 

 

제네릭 너무 어려웡... ㅠ

반응형

'Front-End > TypeScript' 카테고리의 다른 글

TypeScript #13 (OOP 연습 문제 #1~10)  (0) 2025.07.03
TypeScript #12 (OOP 예제 #1~5)  (0) 2025.07.03
TypeScript #11 (객체 지향)  (0) 2025.07.03
TypeScript #10 (함수 유효성 검사)  (0) 2025.07.02
TypeScript #9 (함수)  (0) 2025.07.02