TypeScript 제네릭

김민석·2025년 8월 24일
post-thumbnail

제네릭

제네릭 문법은 타입의 이름을 붙이지 않고 다양한 타입에 대해 동작할 수 있는 포괄적인 코드를 작성할 수 있게끔 해준다.

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

function func(value:string | number): string | number {
	return value;
}

함수에 여러가지 타입을 넣고싶을 때 어떻게 하면 좋을까?? 위에 코드들은 문제가 발생한다.
any 타입은 모든 타입 검사를 무효화하며 any로 선언된 값은 어떤 타입으로도 할당 가능하고, 어떤 속성/메서드를 호출해도 컴파일 단계에서 에러가 나지 않는다.

또한 유니온 타입을 이용하면 타입 좁히기(타입가드,타입단언)을 해야만 메서드를 사용이 가능하다. 이러한 방법을 쓰지않고 제네릭을 사용하면 위의 문제점들을 해결이 가능하다.

제네릭 기본문법

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

let str = func("asda");
let num = func(10);

이런식으로 호출할때 타입의 따라 타입이 변환된다. 제네릭을 사용하면 앞서 제네릭을 사용하지 않았을 때 발생했던 문제점을 말끔히 해결해준다.
동일한 기능을 하는 함수를 중복하여 생산할 필요도 없고, 타입 가드 및 타입 단언을 추가적으로 사용할 필요가 없다.

타입 변수 응용하기

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

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

만약 제네럴을 사용햇는데 swap 프로퍼티로 다른 값을 보내면 오류가 난다. 왜냐하면 T가 string 이엿는데 두번째 프로퍼티는 number이므로 첫번쨰 에서 이미 string으로 되었기 때문에 오류가 난다.

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

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

이런식으로 <T,U> 로 바꿔주면 다른 타입을 받아 사용이 가능하다.

interface Lengthwise {
   length: number;
}

// 제네릭 T 는 반드시 { length: number } 프로퍼티 타입을 포함해 있어야 한다.
function loggingIdentity<T extends Lengthwise>(arg: T): T {
   console.log(arg.length); // 이제 .length 프로퍼티가 있는 것을 알기 때문에 더 이상 오류가 발생하지 않는다.
   return arg;
}

map,forEach 매서드 타입 정의하기

map - 배열 각 요소에 콜백함수를 실행한 후 새로운 배열을 반환해줌
foreach - 배열 각 요소에 콜백함수 실행만 해줌, 반환값 X

Map과 제네릭

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

map함수는 배열과 콜백함수 두 가지를 인수로 받는다.
위는 두 가지 인수 모두 unknown, 반환값 역시 unknownd으로 임시 설정해놓은 상태.

map([1,2,3], (item) => item * 2);
이 경우 매개변수 arr와 map함수 반환값 타입은 number[ ] , 콜백함수의 인수와 반환값 모두 number타입이다. 그래서 위 함수 식을 제네릭 함수로 바꾸면

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

하지만 이렇게 제네릭 타입 변수를 하나만 설정하면 문제가 생긴다.

📌만약에 map([1,2,3] , (it)=>it.toString()) 이라면?
매개변수 arr와 콜백함수 인수는 number, 반환값은 string 타입으로 사용되는 타입이 두 가지 이상이다.
그렇다면 타입 변수를 두 개 이상 써서 반환값의 타입을 분리해야 한다.

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

📌또 만약에 이렇게 map 함수를 호출했다면?

map([100,"string"], (it)=> it.toString())

인수로 들어가는 배열 함수는 넘버타입과 스트링타입이 섞여있다.
이 때 map 함수의 타입 정의는 다음과 같다.

    function map<string | number , string>(arr: (string | number)[], callback:(item: string | number) => string ) : string []

ForEach와 제네릭

forEach 함수는 반환값은 없기때문에 매개변수로 들어가는 배열과 콜백함수의 매개변수 타입만 정리해주면 된다.

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

제네릭 인터페이스

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

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

인덱스 시그니처

interface Map<V> {
	[key:string]:V;
}

let stringMap : Map<string> = {
	key :"value",
}

let numberMap : Map<number> = {
	key :10,
}

이렇게 인덱스 시그니처에 제네릭을 사용하면 하나의 타입으로 다양한
타입을 사용 가능하다.

활용예시

interface Student {
  type: "student";
  school: String;
}

interface Developer {
  type: "developer";
  skill: string;
}

interface User<T> {
  name: string;
  profile: T;
}

let developerUser1: User<Developer> = {
  name: "sono",
  profile: {
    type: "developer",
    skill: "TS",
  },
};

let studentUser1: User<Student> = {
  name: "sono",
  profile: {
    type: "student",
    school: "YC",
  },
};

function goToSchool(user: User<Student>) {
  const school = user.profile.school;
  console.log(`${school}로 등교 완료!`);
}

위 코드와 같이 User 타입에서 profile 프로퍼티의 타입을 학생 또는 개발자로 한정짓지 않고 타입 변수로 정의해준다.
이렇게 하면 타입 가드를 통해 타입 좁히기를 할 필요없이, 매개변수로 들어오는 User의 Profile 타입이 Student라는 것을 명시할 수 있게 된다.

제네릭 클래스


class List<T> {
	construct(private List:T[]) {}
	push(data:T){}
	pop() {}
	print() {}
}

const numberList = new List<number>([1,2,3]);

제네릭을 사용하면 다양한 타입을 가지는 클래스를 쉽게 만들 수 있다.

제네릭과 Promise

const promise = new Promise<number>((resole,reject)=> {

	setTimeout(()=> {
    	resolve(20);
    },3000);
  	reject("sadsadsa");
});

promise.then((response)=> {
	console.log(response *10) // 여기서 unknown이라 타입어로 
})

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


function fetchNumber() : Promise<number> {
	return  new Promise((resole,reject)=> {
	setTimeout(()=> {
    	resolve(20);
    },3000);
  	reject("sadsadsa");
});
}

rresolve는 제네릭을 통해 타입을 설정할 수 있다. reject 자체가 any라서 타입 지정 불가하여 그 결과 catch에서 unknown이 된다.
비동기 함수 에서 타입을 정해줄 떄 비동기 함수의 반환 값을 그 타입을 정해주는 것을 기본적으로 사용한다.

profile
나만의 기록장

0개의 댓글