Home 05-TpyeScript-제네릭
Post
Cancel

05-TpyeScript-제네릭

강의링크


섹션 7. 제네릭

1) 제네릭이란

제네릭 타입은 타입에 유연성을 제공하여 재사용을 가능하게 해주는 타입이다. 타입 정보가 동적으로 결정된다.

즉, 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하여 한번의 선언으로 다양한 타입에 재사용이 가능하다. () 제네릭을 사용하면 함께 작동하는 데이터 구조를 만들거나 다양한 타입의 값을 래핑할 수 있다.)


1
2
3
4
5
//배열타입 생성
const names = ["Max", "Manuel"];

// 에러 발생
const nameA: Array = []

image-20220724113935206

Array라는 타입을 지정하려고 하니, 이는 제네릭 타입이고 하나의 인수가 필요하다고 말하고 있다. 배열은 그 자체로 타입이지만, 배열에 특정 타입의 데이터를 저장할 수 있다. 배열타입은 어떤 타입의 데이터가 저장되든 상관하지 않지만 적어도 정보가 저장되는 것인지에 대해 확인을 하기 때문이다.


1
2
// 홑화살 괄호 내에 배열에 전달되어야 하는 데이터의 타입을 지정한다.
const nameB: Array<string> = []; // string[]과 같다.

이렇게 <>사이에 타입을 지정해주는 것을 제네릭이라고 한다. 배열의 경우에는 배열 내부에 존재하는 타입에 대해서 설정했지만, 타입마다 이는 다르다. 예를 들어 Promise 타입을 살펴보자.

1
2
3
4
5
6
// 문자열을 반환한다. 
const promise: Promise<string> = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("This is done!");
  }, 2000);
});

배열은 특정 타입의 데이터를 저장하기 때문에 해당 내용을 표현을 하게 되고 프로미스는 특정 타입의 데이터를 반환하기 때문에 해당 타입을 <>안에 기입한다.


2) 제네릭 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function merge(objA: object, objB: object) {
    return Object.assign(objA, objB)
};

const mergedObjA = merge({name: "MAX"}, {age:30})

// 접근할 수 없음: object를 반환하지만 name속성이 있는지 모르기때문에  
mergedObjA.name


// 그래서 as를 써서 정의를 할수 있지만, 번거롭다. 
const mergedObjB = merge({name: "MAX"}, {age:30} as {name: string, age: number})

// 접근 가능 
mergedObjB.name

mergedObjA와 같은 상황이 발생하는 이유는 타입스크립트는 객체를 반환하는 것을 추론하지만, 정확히 어떤 정보를 담고 있는지 모르기 때문에 상세히 알려 주어야 하기 때문이다. 해당 객체에서 사용할 수 있는 정보를 모두 담고 있지 않기 때문에 우리는 번거롭게 정보를 자세히 전달해야 한다. 이럴 때 제네릭을 사용하여 타입을 동적으로 할당하여 유연하게 사용할 수 있다.


1
2
3
4
5
6
function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObjB = merge({ name: 'Max', hobbies: ['Sports'] }, { age: 30 });
console.log(mergedObj);

image-20220724120741827

제네릭 타입을 사용하면 이 두 매개변수가 종종 서로 다른 타입이 될 수 있다고 타입스크립트에 알려줄 수 있으므로 무작위의 객체 타입으로 작업하는 것이 아닌 다양한 타입 데이터를 얻고자 한다는 것을 타입스크립트가 인식하게 된다. 또한, 이 함수는 T와 U의 인터섹션을 반환한다고 추론한다. 즉, 이 함수를 정의할 때 이러한 타입들이 고정적으로 설정되지 않고 함수를 호출할 때 동적일 수 있도록 설정한 것이다.

이렇게 된다면, mergeObjB는 name: string과 age: number 객체로 전달한다는 것과 함수가 인터섹션을 반환한다는 것을 알게 된다.

image-20220724120905427

  • T: 객체 타입을 문자열을 지닌 name 속성이 있는 객체로 hobbie 속성은 문자열의 배열로 작성
  • U: 숫자형 타입인 age 속성을 지닌 객체 타입으로 작성

그리고 반환 값인 T&U이기 때문에 해당 객체들의 모든 속성에 접근할 수 있게 된다.


(1) 타입 제약 조건

불필요한 에러나 이상한 작동을 방지하여 최적의 방식으로 제네릭 타입을 제한 하는 것


1
2
3
4
5
6
7
8
// 타입을 제한한다. 
function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: 'Max', hobbies: ['Sports'] }, { age: 30 });
console.log(mergedObj);

Object.assign때문에 매개변수는 객체만 올 수 있기 때문에 제네릭 타입에 특정한 제약 조건을 설정하므로써 객체만 매개변수로 전달될 수 있도록 할 수 있다. 제약 조건에는 무엇이든 지정이 가능하다. 객체, 문자열, 직접 만든 타입, 유니언 타입등 다양한 제약조건을 추가할 수도 있고 U에만 제약을 설정할 수도 있다. 유연하게 제약을 걸 수 있기 때문에 유용한 기능이다.


(2) 속성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 + ' elements.';
  }
  return [element, descriptionText];
}


console.log(countAndDescribe(['Sports', 'Cooking']));

// 에러 발생: number는 length속성이 없기 때문이다.
console.log(countAndDescribe(3));

타입의 속성에 접근할 때, 해당 속성이 있는지 여부를 파악하기 위해서 타입 제약 조건을 활용할 수 있다. length속성의 경우 문자열이나 배열의 타입에 있는 속성이다. 그러나 제네릭을 사용하면 무작위로 타입을 사용할 수 있기 때문에, length속성을 사용할 수 있는 타입스크립트는 알 수 없다. 그래서 타입스크립트에게 우리는 “length속성이 있는 타입을 사용할 거야~”라고 말해 줄 수 있다.

이처럼 보다 유연한 작업이 요구될 때 제네릭 유형을 사용하면 제약 조건 덕분에 정확한 타입에 대해 신경 쓰지 않고 length 속성이 있는지만 신경 쓰면 된다.


(3) keyof

1
2
3
4
// 에러 발생: 여기 입력한 객체가 무엇이든 이 키를 가지는 지 모르기 때문
function extractAndConvert(obj:object, key:string){
  return obj[key];
}

image-20220724140858736

타입스크립트가 이 객체에 이 키가 있는지 보장할 수 없기 때문에 에러가 발생한다. 이를 보장하려면 제네릭 타입을 사용하면 된다.


1
2
3
4
5
function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U) {
  return 'Value: ' + obj[key];
}

extractAndConvert({ name: 'Max' }, 'name');

key of를 활용하여 U타입은 T타입의 key인 것을 명시적으로 알려 줄 수 있다. 즉, keyof 키워드를 지니는 제네릭 타입을 사용하여 정확한 구조를 타입스크립트에게 알려주어 실수를 하지 않도록 도와준다.


3) 제네릭 클래스

클래스를 정의할 때도 메서드나 속성의 타입을 나중에 확정할 수 있다. 즉, 인스턴스를 호출할 때 정한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 원시 타입으로 제한했다.
class DataStorage<T extends string | number | boolean> {
  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>();

이렇게 되면 한 가지 타입으로만 국한되어 클래스 작업을 하지 않아도 된다. 이처럼 데이터의 타입이 무엇이든 상관없다면 제네릭을 통해 유연하게 코드를 작성할 수 있다.


4) 제네릭 유틸리티 타입

TypeScript는 일반적인 타입 변환을 쉽게 하기 위해서 몇 가지 유틸리티 타입을 제공한다.

공식문서

(1) partial

모든 프로퍼티를 선택적으로 타입을 생성한다. (특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다. )


1
2
3
4
5
6
7
8
9
10
11
12
13
interface CourseGoal {
  title: string;
  description: string;
  completeUntil: Date;
}

function createCourseGoal( title: string, description: string, date: Date): CourseGoal {
  let courseGoal: Partial<CourseGoal> = {};  // courseGoal의 타입의 속성을 선택적으로 갖는 객체이다. 
  courseGoal.title = title;
  courseGoal.description = description;
  courseGoal.completeUntil = date;
  return courseGoal as CourseGoal;  // 반환할 때, courseGoal로 형 변환하여 반환한다.
}

courseGoal이 partial 타입이어야 한다고 설정하면 제네릭 타입 덕분에 결과적으로 courseGoal 타입을 지니게 된다. 이렇게 객체나 인터페이스의 속성을 일시적으로 선택적이 되어야 하는 경우가 발생하면, partial타입으로 구현할 수 있다.


(2) Readonly

모든 속성이 읽기 전용(readonly)으로 설정한 타입을 생성한다, 즉 생성된 타입의 프로퍼티는 재할당될 수 없다.


1
2
3
4
5
const names: Readonly<string[]> = ["Max", "Anna"];

// 에러!!
names.push('Manu');
names.pop();


This post is licensed under CC BY 4.0 by the author.

ERROR-Vue3-CompositionAPI에서-동적-컴포넌트-사용하는-방법

ERROR-Vue3-router에서-네비게이션-가드-설정하는-방법