관심있는 언어들/TypeScript

[TypeScript] 제네릭(Generics)

건브로 2022. 1. 27. 16:13

1. 제네릭이란?

다양한 타입에서 작동하는 컴포넌트를 작성할 수 있는 문법이다.

타입스크립트 문서에서는 아래와 같이 정의하고 있다.

C#과 Java 같은 언어에서, 재사용 가능한 컴포넌트를 생성하는 도구 상자의 주요 도구 중 하나는 제네릭입니다, 즉, 단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 수 있습니다. 사용자는 제네릭을 통해 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있습니다.

https://www.typescriptlang.org/ko/docs/handbook/2/generics.html

 

Documentation - Generics

Types which take parameters

www.typescriptlang.org

 

2. 타입이 고정적일 때

//TypeScript
const name = <string>"이름";
console.log(name);
//결과
//이름
// => 이렇게 쓰는 경우는 없을 거다. 다만, dom을 이용해야하는 경우는 필요할 수도 있다.

 

위의 예시는 name 변수를 string 타입으로 밖에 못 쓰도록 해놓았다.

 

사실, 위처럼 사용하는 경우는 거의 없다.

 

왜냐하면, 제네릭은 다양하게 적용해야할 타입에 대해서 불편함 없이 사용하는 데 좋기 때문이다.

 

3. 타입이 고정적이지 않을 때

 

//TypeScript
type Name = [
  {
    name: string;
  }
];

let array: Name= [{name: "park"}];

const insertData = <T extends object>(data: T, array: object[]): object[]  => {
  array.push(data);
  return array;
}
console.log(insertData({name: "gunbro"}, array));

 

여기서 insertData의 data는 T 타입을 갖는데, T 타입은 제네릭 타입변수이며 object를 상속받았다.

 

왜냐하면, Name 타입에 객체 타입만 저장할 수 있다고 alias type으로 정의했기 때문이다.

 

 

//TypeScript

function merge<T, U>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge<{ name: string; hobbies: string[] }, { age: number }>(
  { name: "Park", hobbies: ["Sports"] },
  { age: 25 }
);
console.log(mergedObj);
//결과: {name: "Park", hobbies: ["Sports"], age: 25}

 

위 경우는 최종적으로 Object가 만들어진다.

 

하지만, Object의 프로퍼티 값의 타입은 정해지지 않았다. 

 

이럴 때 제네릭은 빛을 발한다.

 

내가 원하는 타입을 넣을 수가 있어서 실수를 방지할 수 있으며, 동적이다.

 

이점은 Object의 세부적인 프로퍼티 값의 타입은 무엇이 들어갈지

잘 모르기 때문에 안정적이며, 융통성이 있다.

 

4. 제네릭 타입 변수를 key 타입 상속받기

제네릭은 안정성과 융통성을 주는 좋은 문법이다.

 

하지만, 우리가 알고 있던 상식적인 것이 여기서는 엄격하게 설정해야 한다.

 

//TypeScript
function extractAndConvert<T extends object, U extends keyof T>(
  obj: T,
  key: U
) {
  return obj[key];
}

console.log(extractAndConvert({ name: "gunbro" }, "name"));

//결과: gunbro

 

여기서 keyof T가 없다면, 아래와 같은 오류가 발생한다.

'U' 형식을 인덱스 형식 'T'에 사용할 수 없습니다.ts(2536)

 

5. 제네릭 클래스

 

클래스에 여러가지 데이터가 들어간다면, 어떻게 해야할까?

제네릭을 쓰는 게 좋을 것이다.

 

//TypeScript

class DataStorage<T extends number | string | boolean> {
   private data: T[] = [];
   
   addItem(item: T){
      this.data.push(item);
   }
   
   removeItem(item: T){
      this.data.splice(this.data.indexOf(item), 1);
   }
   
   getItems(){
      //1단계 깊은 복사: instance를 그대로 복사해서 사용하더라도 복사된 instace에 피해주지 않기 위해
      return [...this.data];
   }
}

const stringStorage = new DataStorage<string>();
stringStorage.addItem('gunbro');
stringStorage.addItem('youngggo');
stringStorage.addItem('parkminjeong');
stringStorage.removeItem('youngggo');
console.log(stringStorage.getItems());
//결과: ['gunbro', 'parkminjeong']
stringStorage.addItem(10);
//결과: 오류

 

한 번 제네릭 클래스를 정의해두고, 타입을 결정한다면, 그 타입으로 유지해야한다.

 

그렇게 유지하지 않는다면, 아래와 같은 오류가 발생한다.

예) 인자를 string data로 넣지 않고, number data로 넣었을 때

 

'number' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다.

 

6. 유틸리티 타입

 

유틸리티 타입은 이미 정의해놓은 타입을 변환할 때 사용된다.

 

아무리 제네릭을 사용하더라도 유동성의 한계가 있으니, 

유틸리티 타입이 있는 것 같다.

 

6-1. Partial

 

interface Animal{
   name: string;
   age: number;
};

//인터페이스를 이용해서 타입을 부착해주었을 때
//빈 객체로 할당해주면, 오류 발생!
const cat: Animal = {};

//Partial 유틸리티 타입을 이용해서 타입을 부착해주었을 때
//빈 객체로 할당해주어도, 오류 발생하지 않음O
const cat: Partial<Animal> = {};

type Cat = Partial<Animal>;
const munchkin: Cat = {
   name: "코코"
};

const persian: Cat = {
   name: "몽쉘",
   age: 1
};

 

원래 Partial없이 사용했다면, muchkin 할당했을 때부터 오류가 났을것이다.

 

6-2. Pick

//TypeScript

interface Animal{
  name: string;
  age: number;
}
//하나의 속성만 허락할 때
type Cat = Pick<Animal, "name">

const munchkin: Cat = {
   name: "gunbro",
   //name말고는 더 이상 정의 할 수 없다.
}
console.log(munchkin);
//결과: {name: "gunbro"}

//2개 이상의 속성을 허락할 때

type Cat = Pick<Animal, "name" | "age">;

const persian: Cat = {
   name: "몽쉘",
   age: 1,
   //name과 age 속성이 없으면 오류가 난다.
};

 

Pick 타입의 경우는 객체의 프로퍼티에서 꼭 있어야하는 프로퍼티만 따로 타입으로 

만들 수 있음을 보여주는 문법이다. 

 

6-3. Omit

//TypeScript

interface Animal{
  name: string;
  age: number;
}
//Cat을 사용하는 변수는 Omit type에 있는 두 번째 인자인 age를 가질 수 없다.
type Cat = Omit<Animal, "age">

const munchkin: Cat = {
   name: "gunbro",
   //age를 사용하면 오류가 발생한다. 
}
console.log(munchkin);

 

Omit 타입은 Pick 타입과 반대이다.

 

어떤 인터페이스의 프로퍼티들이 많고, 몇 개만 생략하고 싶을 때

Omit 타입을 사용하면 된다.

 

7. Readonly

 

const names: Readonly<string[]> = ["gunbro", "youngggo"];
names.push("Park"); 
//names를 읽기만 가능하기 때문에 2번째 줄은 오류가 발생한다.

 

 

끝!

'관심있는 언어들 > TypeScript' 카테고리의 다른 글

[TypeScript] 고급 타입2  (0) 2022.01.13
[TypeScript] 고급 타입1  (0) 2022.01.06