[TS] 타입스크립트 제네릭(Generic)

seung·2022년 5월 3일
0

제네릭(Generic)이란?

함수 또는 클래스를 정의할 때 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법

제네릭 타입을 사용하면 타입스크립트에게 어떠한 타입을 반환할 것이라고 정보를 줄 수 있고, 보다 나은 타입 안정성을 확보할 수 있다.

< >안에 타입을 지정할 수 있다.

타입 안정성과 결합된 유연성을 제공한다.

<T>를 이용하여 매개변수의 타입과 리턴 타입이 동일하도록 만들어줄 수 있다.

아래와 같은 예시는 함수의 인자로 string이 들어오면 반환값도 string, number가 들어오면 반환값이 number 등 인자와 리턴 타입이 동일하다.

function helloGeneric<T>(message: T): T {
	return message;
}

// 추론
helloGeneric('Mark');
helloGeneric(39);
helloGeneric(true);

// 지정
helloGeneric<string>('Mark');
helloGeneric<number>(39);
helloGeneric<boolean>(true);


변수 타입 지정

const names: Array<string> = []; // string[]
names[0].split(' ');

const promise: Promise<string> = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('This is done!');
  }, 2000);
});


제네릭 함수

제네릭 타입은 매개변수가 정확히 어떤 타입이 될지는 모른다는 추가 정보를 제공하고 서로 다른 타입이 될 수 있다고 알려줄 수 있다.

function merge<T, U>(objA: T, objB: U) { // T & U 가 반환값
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: 'Seung' }, { age: 30 });

제네릭 제약조건

제네릭에 조건을 두어 사용하고 싶다면 extends 키워드로 제약조건을 걸 수 있다.

👉 예제 1

아무 객체가 되어도 상관없지만 일단은 객체여야한다는 조건

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

👉 예제 2

interface Lengthy {
  length: number;
}

function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
  let descriptionText = 'Got no value';

  if (element.length === 1) {
    descriptionText = 'Got 1 element';
  } else if (element.length > 1) {
    descriptionText = `Got ${element.length} element`;
  }
  return [element, descriptionText];
}

👉 예제 3

매개변수 key의 타입은 매개변수 obj의 키값 중에 있어야 한다는 조건

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

extractAndConvert({name: 'seung'}, 'name');
extractAndConvert({name: 'seung'}, 'age'); // 💥 ERROR 객체에 age라는 키값 없으므로 


제네릭 클래스

원하는 타입의 클래스를 만들어서 생성할 수 있다.

아래 예시는 T의 타입이 string, numer, boolean 중에 한 가지만 가능하다는 뜻이다.

class DataStorage<T extends string | number | boolean> {
  private data: T[] = [];

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

  removeItem(item: T) {
    this.data.splice(this.data.indexOf(item), 1);
  }

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


내장 유틸리티 타입


Partial

Readonly

읽는 것만 가능하도록 명시해주는 기법

const names: Readonly<string[]> = ['Seung', 'Max'];


유니온 VS 제네릭


내용을 보다보면 유니온과 제네릭 타입이 유사하게 보이지만 차이점은 무엇인지 예제를 통해 알아보자.

제네릭

해당 클래스의 인스턴스를 생성할때 지정한 하나의 타입만 가능하다.

작업할 때 하나의 타입을 결정하고 고수해야 한다고 입력한 것.

class DataStorage<T extends string | number | boolean> {
  private data: T[] = [];

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

  removeItem(item: T) {
    this.data.splice(this.data.indexOf(item), 1);
  }

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

const storage = new DataStorage<string>(); // string 타입만
storage.addItem('data');
// storage.addItem(123); // 💥 ERROR string 타입 선언했으므로 불가능

유니온

클래스 전체에 하나의 타입이 아니라 각 변수 및 함수에 지정한 유니온 타입 중 가능하다.

메소드를 호출할 때마다 유니온 타입 중 하나를 자유롭게 사용할 수 있다.

class DataStorage {
  private data: (string | number | boolean)[] = [];

  addItem(item: string | number | boolean) {
    this.data.push(item);
  }

  removeItem(item: string | number | boolean) {
    this.data.splice(this.data.indexOf(item), 1);
  }

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

const storage = new DataStorage(); // string 타입만
storage.addItem('data');
storage.addItem(123); // ❗️ 적혀진 유니온 타입 중 하나이므로 가능

keyof


아래와 같은 타입의 객체가 있다.

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

const person: IPerson {
	name: 'Mark',
	age: 39,
};

아래 함수는 위의 객체의 프로퍼티를 get, set 해주는 함수이다.

두 함수에서는 특정한 프로퍼티의 key와 value의 타입은 지정되어 있다.

예를 들면 key가 name일때 value의 타입은 string이고, key가 age 일때 value의 타입은 number이다.

하지만 유니온 타입으로 지정을 해줄 시에 setProp 함수에서는 특정 key에 특정 타입으로 변경이 되는 것이 아니기 때문에 에러가 발생한다.

function getProp(obj: IPerson, key: 'name' | 'age') {
	return obj[key];
}

function setProp(obj: IPerson, key: 'name' | 'age', value: string | number): void {
	obj[key] = value; // 💥 ERROR
}

객체에 keyof를 붙이게되면 결과물이 타입으로 나오는데 key의 이름으로 된 문자열의 유니온 타입으로 만들어진다.

해당 타입으로 위의 함수 타입을 변경하면 일일이 리터럴으로 입력할 필요가 없다.

function getProp(obj: IPerson, key: keyof IPerson): IPerson[keyof IPerson] {
	return obj[key];
}

하지만 keyof를 붙이는 것 까지만 하게 되면, IPerson[’name’] | IPerson[’age’] ⇒ string | number 와 같이 string 또는 number인 유니온 타입이다.

유니온 타입이 아닌 IPerson[’name’] 일 때는 string만, IPerson[’age’] 일 때는 number만 해주고 싶다면?

→ 제네릭을 이용하면 된다.

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
	return obj[key];
}

function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
	obj[key] = value;
}
profile
🌸 좋은 코드를 작성하고 싶은 프론트엔드 개발자 ✨

0개의 댓글