[Generic(제네릭)]

Generic(제네릭)

- Generic이란 일반적인이라는 뜻을 가진 단어로 넓게 해석하면 통용적인, 종합적인 의미로 볼 수도 있다. 따라서 Generic이란 어느 한 타입에 국한되지 않고 다양한 형태의 타입을 수용하여 표현할 수 있는 문법이다.

(*다양한 형태의 타입을 유연하게 받아들인다는 특징 때문에 Polymorphism(다형성)에 사용되는 문법이라고도 칭한다.)

Generic의 사용법은 간단하다. 문법 자체를 완벽히 이해하는데에는 살짝 어렵기도 하고 복잡한 느낌이 들 수도 있지만 가시적으로는 코드가 한결 간결해지는 효과를 볼 수 있다.

function func(value:string){
  return value;
}
let a = func(10);

//Generic
function func<T>(value: T): T {
  return value;
}
let num = func(10);	//typeof num === number
let str = func("a");	//typeof str === string
let bool = func(true);	//typeof bool === boolean

간단하게 원리를 살펴보면 <T>의 형태로 Generic을 사용해주면서 정의된 함수를 사용할 때 argument로 받는 값의 타입을 TS가 파악하여 T에 부여되고 T가 반환값에도 할당이 되면서 자연스레 선언한 변수의 타입으로 지정이 되는 것이다.

위의 코드에서 num 변수를 예로 들어보면 func(10)을 통해 num은 10이라는 값을 할당 받았다. 10은 number 타입이므로 value: number로 할당되며 T가 number이라는 타입을 받게 되며 반환값의 타입인 T도 number을 할당받아 변수 num의 타입은 자동으로 number 타입이 되는 것이다.

 

위와 같이 일반적으로 사용하는 방법도 있지만 명시적으로 Generic을 사용하는 방법도 존재한다. 변수를 선언 후 할당을 해줄 때 argument 앞에 Generic을 입력해주는 방법이 있다. 비교를 위해 타입 단언으로 명시적인 타입 정의에 대한 코드를 함께 살펴보자.

function func<T>(value: T): T{
  return value;
}
//타입 단언
let arr = func([1,2,3] as [number,number,number]);
//Generic
let arr1 = func<[number,number,number]>([1,2,3]);

이렇게 사용하는 경우는 argument의 값의 타입인 T에 먼저 할당되는 것이 아닌 함수명 뒤에 붙은 <T>에 먼저 타입이 할당되고 다음으로 argument의 타입과 반환값의 타입 T에 할당이 된다.

 

[타입 변수 응용]

응용.1 - 함수의 arguments의 타입이 다른 경우 Generic 사용

function swap<T,M>(a:T,b:M){
  return [b,a];
}
const [a,b] = swap("1",2);

함수의 arguments로 받는 값의 타입이 다른 경우 Generic을 2개 이상 사용하여 표현이 가능하다.

 

응용.2 - argument가 배열 형태인 경우 Rest Parameter + Generic 사용

function first<T>(data:[T,...unknown[]]){
  return data[0];
}
let num = first([1,2,3]);
let str = first([1,"a","b","c"]);

argument 값이 배열일 경우 T[ ]와 같은 형태로 작성해도 되지만 특정 index의 값만 필요한 경우 Rest Parameter(나머지 매개변수) 문법을 사용하여 표현이 가능하다.

 

응용.3 - Generic의 특정 Property 존재 여부에 따른 Extends 키워드 사용

function getLength<T extends {length: number}>(data:T){
  return data.length;
}
let one = getLength([1,2,3,4,5]);
let two = getLength("abcde");
let three = getLength({length: 10});
let four = getLength(50); //Error, 50 doesn't have 'length' property

Interface의 확장과 같은 형식의 문법으로 특정 속성을 포함하고 있는 Generic을 사용하여 표현이 가능하다.

[map, forEach method 타입 정의]

Map

- JS에서 많이 사용했던 배열 전용 method로 특정 배열을 map method를 통해 순회하며 그에 맞는 callback 함수를 입력해주면 callback 함수를 거쳐 새로운 배열을 반환해주는 method이다. 코드를 통해서 Generic 문법을 사용하여 map method와 동일한 기능을 하는 함수를 만들어보자.

//Javascript
const arr = [1,2,3];
const newArr = arr.map(e => e * 2);
console.log(newArr) //[2,4,6]
//Typescript
function map<T,M>(arr: T[],callback:(item: T) => M){
  let result = [];
  for(let i = 0; i < arr.length; i++){
    result.push(callback(arr[i]))
  }
}
map(arr,(i) => i * 2));
map(['a','b'], (i) => i.toUpperCase()); //typeof i === string
map(['a','b'], (i) => parseInt(i)); //typeof i === number

 

ForEach

- Map method와 비슷하게 배열을 순회하며 요소에 특정 작업을 하는 method이지만 map method와 다르게 forEach method는 새로운 배열을 반환하지 않고 기존 배열을 변화시킨다는 차이점이 있다. forEach method는 반환값이 없기 때문에 상대적으로 쉽게 함수로 표현이 가능하다.

//Javascript
const arr = [1,2,3];
arr.forEach(e => e * 2);
console.log(arr) // [2,4,6]
//Typescript
function forEach<T>(arr:T,callback:(item:T) => void){
  for(let i = 0; i < arr.length; i++){
    callback(arr[i])
  }
}
forEach(arr,(i) => console.log(i.toFixed()));
forEach(['a','b'], (i) => i);

 

[Generic Interface & Generic Type Alias]

Generic Interface

- Generic Interface란 말그대로 InterfaceGeneric 문법으로 사용하는 방식을 말한다. Generic Interface는 타입으로 사용할 때 반드시 타입 내부에 할당할 타입을 꺽쇠와 함께 표기를 해주어야 한다.

interface KeyValue<T,M> {
  key:T,
  value: M 
}
let keyvalue: KeyValue<string,number> = {
  key:"key",
  value: 1
}
let keyvalue1:KeyValue<string,number[]> = {
  key:"key",
  value: [1]
}

Generic에서 꺽쇠 내부에 있는 변수를 공식 문서에 따라 Type variable(타입 변수)라고 하는데 다른 이름으로는 Type Parameter,Generic Type Variable, Generic Type Parameter등 다양한 이름으로 불린다.

 

Index Signature + Generic Interface

이전에 알아보았던 Index Signature와 Generic Interface를 사용하여 문법 작성도 가능하다. Index Signaturepropertykey와 value의 규칙만 만족한다면 객체를 허용하는 유연한 객체 타입 정의 방식이다. Generic Interface와 함께 사용하는 예시 코드를 보자.

//Index Signature
interface A {
  [key:string]: number
}
let a:A = {
  key: 1
}
//Index Signature + Generic Interface
interface B<T> {
  [key:string]: T
}
let b:B<number> = {
  key: 10
}
let b1:B<boolean> = {
  key: true
}

 

Generic Type Alias

Type AliasGeneric을 이용해 작성하는 문법은 Interface를 활용하는 것과 매우 비슷하다.

type C<T> = {
  [key:string] : number
}
let c:C<number> = {
  key: 2
}

type D<M> = M;
let d:D<number> = 213;
let e:D<number> = 'abc'; //Error

 

Generic Interface의 활용

위와 같이 문법 구조에 대해서는 알아보았지만 어느 상황에 유용하게 쓰이는지 감이 오지 않을 수 있다. Generic Interface는 객체 타입들로 조합된 복잡한 객체 타입을 정의하여 사용할 때 코드를 깔끔하고 유연하게 사용할 수 있다. 사용 예시를 간단한 코드 예시를 보며 이해해보자.

interface Student {
  type: "student",
  school: string
}

interface Developer {
  type: "developer",
  skill: string
}
interface User<T> {
  name: string,
  profile: T
}
function goSchool(user:User<Student>){
  const school = user.profile.school;
  console.log(`Go To ${school}`);
}
goSchool(developerUser) //Error

const developerUser:User<Developer> = {
  name: "Tom",
  profile: {
    type: "developer",
    skill: "typescript"
  }
}
const studentUser:User<Student> = {
  name: "Bob",
  profile: {
    type: "student",
    school: "school"
  }
}

[Generic Class]

Generic Class

- Class를 선언할 때 Generic 문법을 사용하여 선언하여 활용성을 높이는 방식이 존재한다. 앞서 Generic에 대해 많이 살펴보았으니 코드만 보아도 어떤 식으로 동작하는지 쉽게 이해가 가능할 것이다.

class List<T> {
  constructor(private list:T[]){}
  push(data:T){
    this.list.push(data)
  }
  pop(){
    this.list.pop()
  }
  print(){
    console.log(this.list)
  }
}
const list = new List<number>([1,2,3]);
list.push(4);
list.pop();
list.print();

const list1 = new List<string>(['a','b','c']);
list.push('d');
list.pop();
list.print();

[Promise & Generic]

Promise & Generic

- Promise 객체는 JS에 내장되어 있는 객체로 비동기 통신에 사용하는 객체이다. Promisecallback 함수를 받으며 Promise가 성공적으로 반환되면 then method를 사용하여 처리하고 반환에 실패하면 catch method를 사용하여 통신 결과를 처리할 수 있는 객체이다. Promise는 비동기 작업의 결과값의 타입을 자동으로 추론할 수 없다. 따라서 반환을 성공적으로 받은 경우 실행하는 resolve 함수에는 비동기 통신의 결과값을 argument로 전달을 할 수 있는데 arguments 값의 타입을 Generic을 통해서 정의해줄 수 있다.

const promise = new Promise<number>((res,rej) => {
  setTimeout(() => {
    res(20);
  },2000);
});
promise.then(() => {
  res(20)
})
.catch(() => {
  console.log("error");
});

위의 코드에서 성공에 해당하는 res 함수의 argument 값의 타입을 Generic 타입을 통해 number type을 부여해주었다.

Promise는 Generic Class를 기반으로 타입이 선언되어 있기 때문에 비동기 처리의 결과값의 타입을 지정해줄 수는 있지만, 실패했을 경우에는 타입을 정의해줄 수 없다. 추가로 타입을 지정해주지 않았을 경우에는 자동적으로 Unknown Type으로 추론이 된다.

 

Promise를 반환하는 함수 예시

간단한 예시로 Promise를 반환하는 함수를 예시로 들어 반환값에 대한 타입을 정의해보는 코드를 작성해보자.

interface Post {
  id: number;
  title: string;
  content: string
}
//#1
function fetchPost():Promise<Post>{
  return new Promise((res,rej) => {
    setTimeout(() => {
      res({
        id: 1,
        title: "title",
        content: "content"
      })
    },2000)
  })
}
//#2
function fetchPost(){
  return new Promise<Post>((res,rej) => {
    setTimeout(() => {
      res({
        id: 1,
        title: "title",
        content: "content"
      })
    },2000)
  })
}
const poseReq = fetchPost();
poseReq.then((post) => {
  post.id
})

fetchPost 함수를 통해 Promise 객체를 반환 받으며 함수 처리가 성공했을 경우 post 객체의 id property를 가져오는 코드를 작성해보았다. 이 때 fetchPost 함수의 반환값에 대한 타입 처리를 해주지 않는 경우 자동적으로 반환값에는 Unknown Type이 추론되기 때문에 post.id로 접근을 하면 오류가 발생한다. 따라서 위와 같이 반환되는 Promise의 타입을 Generic을 통해서 정의해주어야 하는데 타입 정의 방식은 함수의 반환값에 정의를 하는 방법과 Promise에 직접 정의를 해주는 두가지 방법이 있다. 두가지 방법 모두 좋은 방법이지만 코드 작업 자체는 기본적으로 협업이 우선 시 되므로 첫번째 방법을 추천한다.

'Typescript' 카테고리의 다른 글

[TypeScript] - (9) 조건부 타입  (1) 2024.08.22
[TypeScript] - (8) Type 조작  (0) 2024.08.14
[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (4) Typescript 함수  (0) 2024.07.31

+ Recent posts