TypeScript 강의 정리: 제네릭

zeroequaltwo·2022년 10월 7일
0

TS

목록 보기
7/8
post-thumbnail

1. 제네릭이란(Generics)?

  • 제네릭은 선언 시점이 아닌 생성 시점에 타입을 명시하여 여러 데이터 타입에 대해 클래스/인터페이스/함수가 동일하게 동작할 수 있게 해주는 기능이다.
  • 제네릭 타입은 TS에만 있지 JS에는 없는 개념이다.
    참고: 제네릭1
    참고: 제네릭2

2. 제네릭 함수

1) 제네릭 함수 작성하기

  • 함수에 꺽쇠괄호 안에 제네릭 타입(T)을 입력하고, 파라미터에도 제네릭 타입을 할당한다.
  • T말고 다른 걸로 입력해도 되는데 관례상 T로 쓴다고 한다. 아래는 두 파라미터의 객체값이 다를 수도 있다는 전제를 갖고있어서 T & U로 적었다.
  • 제네릭 함수를 쓰지 않고 그냥 any를 써버릴 수도 있지만 그러면 리턴되는 값이 뭔지 알 수 없게 된다.
// 파라미터 두 개가 모두 Object 타입이니 함수 리턴값도 object일 거라 추론한다.
function merge(objA: object, objB: object) {  
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Max" }, { age: 30 });
// object 타입은 모든 타입을 할당할 수 있으므로 ts는 각 프로퍼티가 어떤 타입인지 알 수 없어 에러가 난다.
console.log(mergedObj.age);  

▽▽▽

function merge<T, U>(objA: T, objB: U) {  // return 타입은 T & U
  return Object.assign(objA, objB);
}

// 함수를 호출할 때 구체적인 타입을 지정해줘도 된다.
const mergedObj = merge<{name: string}, {age:number}>({ name: "Max" }, { age: 30 });
console.log(mergedObj.age); // 에러 발생 X

2) 제약조건 작성하기

  • 아무 타입이나 와도 될 때 제네릭 타입을 사용하지만 그럼에도 최소한의 제약조건이 필요한 경우 extends를 통해 타입의 최소조건을 설정할 수 있다.
/*----- 예시 1 -----*/
function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);  // 객체를 병합하는 메소드이기 때문에 최소한 둘다 객체여야한다.
}

const mergedObj = merge({ name: "Max" }, 30);  // 30은 object 타입이 아니라고 에러 난다.
console.log(mergedObj);

/*----- 예시 2 -----*/
interface Lengthy {
  length: number;
}
// length 속성을 가진 타입만 할당할 수 있다. 
function countAndDescription<T extends Lengthy>(element: T): [T, string] {
  let description = "Got no value";
  if (element.length > 0) {
    description = `Got ${element.length} value(s)`;
  }
  return [element, description];
}

console.log(countAndDescription("Hi!"));  // ['Hi!', 'Got 3 value(s)']
console.log(countAndDescription([]));  // [Array(0), 'Got no value']
// console.log(countAndDescription(100));  // length 속성이 없는 타입이므로 에러가 난다.
  • keyof 제약조건을 통해 객체의 key값을 조건으로 넣을 수도 있다.
function extractAndConvert(obj: object, key: string){
  return obj[key];  // object가 key값을 갖고있는지 아닌지 알 수 없다고 에러가 난다. 
}

▽▽▽

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

// extractAndConvert({}, name);  // 이제 함수 내 리턴이 아니라 여기서 에러가 난다.
console.log(extractAndConvert({ name: "Max" }, "name"));  // Max

3. 제네릭 클래스

  • 원시 자료형: 실제 데이터를 저장하는 데이터 타입
class DataStorage<T> {
  private data: T[] = [];

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

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

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

const textStorage = new DataStorage<string>();
textStorage.addItem("Max");
textStorage.addItem("Anna");
textStorage.addItem("Sue");

textStorage.removeItem("Anna");
console.log(textStorage.getItem());  // ['Max', 'Sue']
  • 참조 자료형: 객체의 번지를 참조하는 타입으로 메모리 번지 값을 통해 객체를 참조하는 타입
  • 주소 문제로 인해 제네릭 타입은 참조 자료형보다는 원시 자료형과 쓰는 것이 더 안전하다.
    참고: 자료형 분류
class DataStorage<T> {
  private data: T[] = [];

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

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

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

const textStorage = new DataStorage<object>();
textStorage.addItem({ name: "Max" });
textStorage.addItem({ name: "Anna" });
textStorage.addItem({ name: "Sue" });

textStorage.removeItem({ name: "Anna" });
console.log(textStorage.getItem());  // 0: {name: 'Max'}, 1: {name: 'Anna'} 
// addItem의 Anna 객체와 removeItem의 Anna객체는 같아 보이지만 다른 주소를 갖고있는 다른 값이다.
// 따라서 indexOf() 메소드가 같은 값을 찾지 못하기 때문에 -1 값을 리턴하고 마지막 요소가 사라진 것이다.

▽▽▽

class DataStorage<T> {
  private data: T[] = [];

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

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

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

const textStorage = new DataStorage<object>();
textStorage.addItem({ name: "Max" });
const AnnaObj = { name: "Anna" };
textStorage.addItem(AnnaObj);  // 같은 주소를 인지할 수 있게끔 변수에 할당한다.
textStorage.addItem({ name: "Sue" });

textStorage.removeItem(AnnaObj);
console.log(textStorage.getItem()); // 0: {name: 'Max'}, 1: {name: 'Sue'}

4. 제네릭 유틸리티 타입

: 제네릭 타입을 사용하는 내장 타입

1) Partial

생성된 타입의 모든 프로퍼티를 옵셔널로 만든다.

interface CourseGoal {
  title: string;
  description: string;
  completeUntil: Date;
}

function createCourseGoal(
  title: string,
  description: string,
  date: Date
): CourseGoal {
  let courseGoal: CourseGoal = {};  // 있어야 할 프로퍼티가 없다고 에러난다.
  courseGoal.title = title;
  courseGoal.description = description;
  courseGoal.completeUntil = date;
}

▽▽▽

interface CourseGoal {
  title: string;
  description: string;
  completeUntil: Date;
}

function createCourseGoal(
  title: string,
  description: string,
  date: Date
): CourseGoal {
  let courseGoal: Partial<CourseGoal> = {};
  courseGoal.title = title;
  courseGoal.description = description;
  courseGoal.completeUntil = date;
  // Partial<CourseGoal>과 CourseGoal은 다르므로 리턴 값은 CourseGoal로 지정해준다.
  return courseGoal as CourseGoal;  
}

2) Readonly

  • 초기값 설정 이후 데이터를 수정할 수 없게 한다.
const names: Readonly<string[]> = ["Max", "Anna"];
// names.push("Sue");  // readonly라고 에러가 난다.

5. 제네릭 타입 vs 유니언 타입

  • 여러 타입을 혼용한다는 점에서 두 타입 사이에 유사성이 있다고 생각할 수 있지만 실제로 이 둘은 다른 기능을 한다.
  • 유니언 타입은 모든 메소드 혹은 함수 호출 때마다 다른 타입을 지정하고자 할 때 사용하고, 제네릭 타입은 여러 선택지 중 하나를 선택해 그 타입을 끝까지 고수할 때 사용한다. 예를 들어 string과 number를 유니언 타입으로 준 배열은 ["A", 1, "B", 2]와 같이 두 타입을 혼용해서 push 할 수 있지만, string과 number를 extends한 제네릭 타입은 ["A", "B"] 혹은 [1, 2]와 같은 배열만 만들어낼 수 있다는 것이다.
profile
나로 인해 0=2가 성립한다.

0개의 댓글