[TS] Chapter 7. Typescript 제네릭(Generics)_1

변진상·2023년 6월 6일
0

Typescript 학습

목록 보기
11/13

목표: 제네릭에 대해 학습한다.

  • Generic Functions
  • Constraints
  • Generic Class

Generic이란?

제너릭은 선언 시점이 아닌 생성 시점에 타입을 명시하여 다양한 타입을 사용할 수 있도록 하는 기법이다.(출처: https://poiemaweb.com/typescript-generic)
제너릭은 type safety와 유연성을 동시에 제공한다.

// 제너릭 선언 형식
nameOfSomething<T>
// 이름<타입 파라미터>

내장 제네릭

  • Array
  • Promise
// generics

const names: Array<string> = ["otter", "dog"];

const tgmp = names[1].concat("jijijijijijijiji", "fjidjdiji");
// => dogjijijijijijijijifjidjdiji

const promise: Promise<number> = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(13333330);
  }, 2000);
});

promise.then((data) => {
  console.log(data.toLocaleString());
});

제너릭 함수

제너릭 함수는 parameter에 제너릭을 사용하는 것인데, 해당 매개변수가 종종 다른 타입이 될 수 있다고 타입스크립트에 알려줄 수 있다. 이 경우 타입패러미터는 인자의 종류에 따라 결정된다.

// 제너릭 함수

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

const mergedObj = merge({ name: "nn" }, { age: 11 });
console.log(mergedObj);
const mergedObj = merge<{ name: string }, { age: number }>(
  { name: "nn" },
  { age: 11 }
);
console.log(mergedObj);

두 번째 코드와 같이 인자들의 타입을 미리 선언하는 경우도 있다.

constraints

만약 아래와 같이 인자가 주어졌다고 가정해보자.

console.log(merge({ name: "otter" }, 30));

이 경우 30은 객체가 아니라서 기대하는 결과가 나오지 않을 것이다. T, U가 어떤 타입이냐 객체의 정확한 구조도 상관 없이 아무 타입이라도 상관없기 때문이다. 이런 문제를 해결하기위해 제너릭에 제약을 걸어주는데 extends 키워드를 사용한다.

용법: <T extends Type(primitive types, custom type, union, ...)>

function merge<T extends object, U extends object>(a: T, b: U) {
  return Object.assign(a, b);
}


사진과 같이 저장을 하려면 컴파일러 에러를 띄우고 TS에서 에러를 감지해준다.

constraints와 generic function 응용

interface Lengthy {
  length: number;
}
// 제너릭 패러미터의 제약조건으로 사용할 인터페이스를 만들었다.

function countAndDescribe<T extends Lengthy>(element: T): [T, string] {// 제너릭을 이용해 length property를 가지는 객체가 인자로 들어올 것을 제약조건으로 걸어줬다. 그리고 return type이 튜플이 될 수 있도록 리턴 타입을 정해줬다.
  let description = "Got no value.";
  if (element.length === 1) {
    description = "Got 1 element.";
  } else if (element.length > 1) {
    description = "Got " + element.length + " elements.";
  }

  return [element, description];
}

console.log(countAndDescribe(["djlfkajsdlj", "dfjioadsijfoisdj"]));

keyof

만약 객체와 키를 받아 그 값을 출력하는 함수가 있다고 가정했을 때, 객체에 인자로 주어진 키가 없다면 기대하는 결과를 얻기 어려울 것이다. 이를 제너릭을 통해 체크할 수 있다. 아래와 같이 keyof 키워드를 사용하면 된다.

<T: object, U: extends keyof T>
  
function extractValue(obj: object, key: string) {
  console.log(obj[key]);
}
// 위 코드는 object 내에 key가 존재할지 보장하지 못한다.

function extractValue<T extends object, U extends keyof T>(obj: T, key: U) {
  console.log(obj[key]);
}
// 이렇게 제너릭과 keyof 키워드를 사용하면
//객체에 존재하지 않는 key가 주어졌을 경우 TS에서 에러를 띄운다.

generic class

클래스 내에 property나 메소드에 사용될 제너릭도 사용할 수 있다.

class className<T extends number | string | boolean> {
	private data: T[] = [];
  
  method(parameter: T){
    ...
  }
}
// generic class
class DataStorage<T extends string | number | boolean> {
// 클래스에 string | number | boolean만을 가지는 T라는 제너릭 패러미터를 가진 제너릭을 사용했다. 아래 배열과 메소드의 패러미터가 가질 타입에 대한 정보를 받아와 TS에게 알려준다.
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    if (this.data.indexOf(item) === -1) {
      return;
    }
    this.data.splice(this.data.indexOf(item), 1); // -1
  }

  getItems() {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("Max");
textStorage.addItem("Manu");
textStorage.removeItem("Max");
console.log(textStorage.getItems());

const numberStorage = new DataStorage<number>();

틈새 디버깅

const objStorage = new DataStorage<object>();
//const maxObj = {name: 'Max'};
//objStorage.addItem(maxObj); 
objStorage.addItem({name: 'Manu'});
// ...
objStorage.removeItem({name: 'Max'});
// objStorage.removeItem(maxObj);// 이렇게 하면 제거 가능... 
console.log(objStorage.getItems());

이 코드의 경우 배열의 마지막 요소만 지워지는 것을 확인 할 수 있는데, 이는 참조 타입인 객체의 주소를 정확하게 알 수 없어 indexOf 메서드가 -1을 리턴해 배열의 마지막 요소를 제거한다...

profile
자신을 개발하는 개발자!

0개의 댓글