[TypeScript] 제네릭

Jeris·2023년 6월 9일
0

TypeScript

목록 보기
7/11
post-custom-banner

제네릭

제네릭(Generic)이란 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 문법입니다.

// 일반 함수
function func(value: any) {
  return value;
}

let num = func(10); // num: any

let bool = func(true); // bool: any

let str = func("string"); // str: any

function func1(value: unknown) {
  return value;
}

str.toUpperCase(); // ❌ No, str: unknown
num.toFixed(); // ❌ No, num: unknown

// 제네릭 함수
function func2<T>(value: T): T { // T: type variable
  return value;
}

let num func2(10); // num: number

let bool = func2(true); // bool: boolean

let str = func2("string"); // str: string

let arr = func<[number, number, number]>([1, 2, 3]); // 타입 변수를 튜플 타입으로 할당

타입 변수 응용하기

swap

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

const [a, b] = swap("1", 2); // T: string, U: number

returnFirstValue

function returnFirstValue<T>(a: [T, ...unknown[]]) { // 튜플
  return a[0];
}

let num = returnFirstValue([0, 1, 2]); // num: number
// 0

let str = returnFirstValue(["hello", "mynameis"]); // str: string
// hello

let num2 = returnFirstValue([1, "hello", "mynameis"]); // num2: number
// 1

getLength

function getLength<T extends { length: number }>(data: T) { // length 프로퍼티를 필수로 만듭니다.
  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); // ❌ No

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

배열 메서드인 map()forEach()는 다양한 타입의 요소를 가진 배열에 모두 사용할 수 있으므로, 이를 타입스크립트에서 제네릭으로 표현할 수 있습니다.

map 메서드

map 메서드는 배열의 모든 요소에 대해 주어진 함수를 호출하고, 그 결과로 새로운 배열을 생성합니다. 이 때 각 요소의 타입은 제네릭을 사용하여 지정할 수 있습니다.

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;
}

const arr = [1, 2, 3];
map(arr, (it) => it * 2); // it: number
map(['hi', 'hello'], (it) => it.toUpperCase()); // it: string

forEach 메서드

forEach 메서드는 배열의 모든 요소에 대해 주어진 함수를 실행합니다. 이 때 각 요소의 타입은 제네릭을 사용하여 지정할 수 있습니다.

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

const arr = [1, 2, 3];
forEach(arr, (it) => {
  console.log(it.toFixed());
});

forEach(['123', '456'], (it) => { return it; }) // it: string

제네릭 인터페이스

제네릭을 인터페이스에 적용하면, 인터페이스에 사용되는 특정 타입을 유연하게 정의할 수 있습니다.

제네릭 key-value 쌍 인터페이스

interface KeyPair<K, V> {
  key: K;
  value: V;
}

let keyPair: KeyPair<string, number> = { // generic function과 달리 타입 변수에 타입을 할당해야 합니다.
  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> { // value의 타입을 타입 변수로 할당
  [key: string]: V;
}

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

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

제네릭 타입 별칭

타입 별칭(type alias)은 특정 타입이나 인터페이스에 이름을 붙이는 것을 말합니다. 타입 별칭은 복잡한 타입을 더 간결하게 표현할 수 있게 해주며, 제네릭을 적용하면 더욱 유연한 타입 표현이 가능해집니다.

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

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

제네릭 인터페이스 활용 예제

interface Student {
  type: "student";
  school: string;
}

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

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

function goToSchool(user: User<Student>) {
  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: "가톨릭대학교",
  },
};

제네릭 클래스

제네릭을 사용하면 클래스에서 작동하는 타입을 선언 시점이 아닌 인스턴스화 시점에서 결정할 수 있습니다.

// 일반 클래스
class NumberList {
  constructor(private list: mnumber[]) {}
  
  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();

// 제네릭 클래스
class List<T> {
  constructor(private list: T[]) {}
  
  push(data: T) {
    this.list.push(data);
  }
  
  pop() {
    return this.list.pop();
  }
  
  print() {
    console.log(this.list);
  }
}

const List1 = new List([1, 2, 3]); // List<number>를 안써도 됩니다.
List1.pop();
List1.push(4);
List1.print();

const List2 = new List(["1", "2");
List2.push("hello");

프로미스와 제네릭

프로미스(Promise)는 자바스크립트에서 비동기 처리를 간편하게 할 수 있도록 설계된 객체입니다. 이는 비동기 작업이 완료된 이후의 결과값을 나타냅니다. 타입스크립트에서는 제네릭을 사용하여 프로미스가 반환하는 결과값의 타입을 지정할 수 있습니다.

const promise = new Promise<number>((resolve, reject) => {
  setTimeout(() => {
    resolve(20);
    // reject("");
  }, 3000);
});

promise.then((res)=>{
  console.log(res * 10); // 200
});

promise.catch((err) => {
  if (typeof err === "string") { // err 객체는 타입 좁히기를 사용해야 합니다.
    console.error(err);
  }
});

위의 예제에서 new Promise<number>Promise<number> 타입의 프로미스를 생성합니다. 이 프로미스는 숫자 값을 반환합니다. then 메서드는 이 프로미스가 성공적으로 완료되었을 때 호출됩니다. 여기서 res의 타입은 number입니다.

프로미스를 반환하는 함수를 선언할 때도 반환 타입을 Promise<T> 형태로 선언할 수 있습니다.

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: Post
  post.id;
});

Reference

profile
job's done
post-custom-banner

0개의 댓글