본문 바로가기
프로그래밍/TypeScript

타입스크립트 제네릭

by hi-rachel 2023. 10. 23.

제네릭

제네릭 소개

/**
 * 제네릭
 * = 일반적인
 * 모든 타입에 두루두루 쓸 수 있는 범용 (함수)
 */

// 제네릭 함수
// 함수에 인수에 따라서 반환값의 타입을 가변적으로 정해줄 수 있다!
function func<T>(value: T): T {
  return value;
}

let num = func(10);

let bool = func(true);

let str = func("string");

let arr = func<[number, number, number]>([1, 2, 3]);
// let arr = func<[number, number, number]>([1, 2, 3] as [number, number, number]);

타입 변수 응용하기

/**
 * 첫 번째 사례
 */

function swap<T, U>(a: T, b: U) {
  return [b, a];
}

const [a, b] = swap("1", 2);

/**
 * 두 번째 사례
 */

// tuple type은 특정 인덱스에 해당하는 타입을 지정할 수 있음
// 첫 번째 요소 이후에 들어오는 요소는 무슨 타입인지, 몇 개인지 알 필요가 x -> ...unknown[]
function returnFirstValue<T>(data: [T, ...unknown[]]) {
  return data[0];
}

let num = returnFirstValue([0, 1, 2]);
console.log(num);

let str = returnFirstValue([1, "hello", "myNameIs"]);
console.log(str);

/**
 * 세 번째 사례
 */

// 아래 extends 의미와 똑같음
// interface InterfaceA {
//     length: number;
// }

// interface InterfaceB extends InterfaceA {
// }

function getLength<T extends { length: number }>(data: T) {
  return data.length;
}

let var1 = getLength([1, 2, 3]); // 3

let var2 = getLength("12345"); // 5

let var3 = getLength({ length: 10 }); // 10

// let var4 = getLength(10); X

map, forEach 메서드 타입 정의하기

/**
 * map 메서드
 */

const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2); // [2, 4, 6]

function map<T, U>(arr: T[], callback: (item: T) => U) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i]));
  }
  return result;
}

map(arr, (it) => it * 2); // [2, 4, 6]
map(["hi", "hello"], (it) => it.toUpperCase()); // [ 'HI', 'HELLO' ]
map(["hi", "hello"], (it) => parseInt(it)); // [ NaN, NaN ]

/**
 * forEach
 */

const arr2 = [1, 2, 3];
arr2.forEach((it) => console.log(it));

function forEach<T>(arr: T[], callback: (item: T) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}

forEach(arr2, (it) => {
  console.log(it.toFixed());
});

forEach(["123", "456"], (it) => {
  it;
});

제네릭 인터페이스 & 제네릭 타입 별칭

/**
 * 제네릭 인터페이스
 */

// 타입 변수 = 타입 파라미터 = 제네릭 타입 변수 = 제네릭 타입 파라미터
interface KeyPair<K, V> {
  key: K;
  value: V;
}

// 제네릭 인터페이스를 사용할 때는 반드시 타입 정의시 변수에 할당할 타입을 <>와 함께 명시해주어야 한다.
let keyPair: KeyPair<string, number> = {
  key: "key",
  value: 0,
};

let keyPair2: KeyPair<boolean, string[]> = {
  key: true,
  value: ["1"],
};

/**
 * 인덱스 시그니쳐
 */
interface NumberMAP {
  [key: string]: number;
}

let numberMap1: NumberMAP = {
  key: -1231,
  key2: 123123,
};

// 하나의 타입으로 다양한 객체 표현 가능
interface Map<V> {
  [key: string]: V;
}

let stringMap: Map<string> = {
  key: "value",
};

let booleanMap: Map<boolean> = {
  key: true,
};

/**
 * 제네릭 타입 별칭
 */

type Map2<V> = {
  [key: string]: V;
};

let stringMap2: Map2<string> = {
  key: "hello",
};

/**
 * 제네릭 인터페이스의 활용 예시
 * -> 유저 관리 프로그램
 * -> 유저 구분 : 학생 유저 / 개발자 유저
 */

// 서로소 집합
interface Student {
  type: "student";
  school: string;
}

interface Developer {
  type: "developer";
  skill: string;
}

interface User<T> {
  name: string;
  profile: T;
}

function goToSchool(user: User<Student>) {
  // 제네릭 인터페이스 이용하면 타입 좁히기 없어도 됨
  //   if (user.profile.type !== "student") {
  //     console.log("학생만 출입 가능합니다.");
  //     return;
  //   }

  const school = user.profile.school;
  console.log(`${school}로 등교 완료`);
}

const developerUser: User<Developer> = {
  name: "이정환",
  profile: {
    type: "developer",
    skill: "TypeScript",
  },
};

const studentUser: User<Student> = {
  name: "홍길동",
  profile: {
    type: "student",
    school: "MIT",
  },
};

제네릭 클래스

/**
 * 제네릭 클래스
 */

//  NumberList에 다른 타입을 받는 List를 만들려면 중복이 일어남 -> 제네릭 클래스로 다양한 타입을 사용하는 List 정의할 수 있음
class List<T> {
  constructor(private list: T[]) {}

  push(data: T) {
    this.list.push(data);
  }

  pop() {
    return this.list.pop();
  }

  print() {
    console.log(this.list);
  }
}

class NumberList {
  constructor(private list: number[]) {}

  push(data: number) {
    this.list.push(data);
  }

  pop() {
    return this.list.pop();
  }

  print() {
    console.log(this.list);
  }
}

const numberList = new NumberList([1, 2, 3]);
numberList.pop();
numberList.push(4);
numberList.print(); // [1, 2, 4]

// 제네릭 클래스는 제네릭 인터페이스나 제네릭 타입 변수와 다르게 클래스의 생성자를 호출할 때 생성자의 인수로 전달하는 값을 기준으로 자동 타입 추론
const list = new List(["hi", 1, 2, true]);
list.print(); // [ 'hi', 1, 2, true ]

프로미스와 제네릭

/**
 * 프로미스
 */

// 프로미스는 제네릭 클래스를 기반으로 타입이 선언되어 있기 때문에 타입 변수로 비동기 처리의 결괏값 타입을 정해줄 수 있지만
// reject 실패했을 때 타입은 정해줄 수 x
// 타입 변수 정하지 않으면 -> 기본 unknown
const promise = new Promise<number>((resolve, reject) => {
  setTimeout(() => {
    resolve(20);
    reject("~ 때문에 실패");
  }, 3000);
});

promise.then((response) => {
  console.log(response * 10); // 20
});

promise.catch((err) => {
  if (typeof err === "string") {
    console.log(err);
  }
});

/**
 * 프로미스를 반환하는 함수의 타입을 정의
 */

interface Post {
  id: number;
  title: string;
  content: string;
}

function fetchPost(): Promise<Post> {
  return new Promise<Post>((resolve, reject) => {
    setTimeout(() => {
      resolve({
        id: 1,
        title: "게시글 제목",
        content: "게시글 컨텐츠",
      });
    }, 3000);
  });
}

const postRequest = fetchPost();

postRequest.then((post) => {
  post.id;
});

[참고]

한 입 크기로 잘라먹는 타입스크립트

  • Section 7. 제네릭 강의를 듣고 정리한 내용입니다.

'프로그래밍 > TypeScript' 카테고리의 다른 글

조건부 타입  (1) 2023.10.24
타입 조작하기  (0) 2023.10.24
타입스크립트 클래스  (1) 2023.10.23
타입스크립트 인터페이스  (0) 2023.10.23
타입스크립트 함수와 타입  (0) 2023.10.18