Frontend/TypeScript

[TypeScript] 6-2. 제네릭 활용

ayeongjin 2025. 1. 28. 03:06

제네릭 인터페이스

인터페이스에 타입 변수를 선언해 사용

 

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"],
};
  • 변수 keyPair의 타입으로 KeyPair<string, number>를 정의
    • 그 결과 K에는 string, V에는 number 타입이 각각 할당되어 key 프로퍼티는 string 타입이고 value 프로퍼티는 number 타입인 객체 타입이 된다.
    • 따라서 값으로 해당 타입의 객체를 저장
  • 변수 keyPair2의 타입으로 KeyPair<boolean, string[]>를 정의
    • 그 결과 K에는 boolean, V에는 string[] 타입이 각각 할당되어 key 프로퍼티는 boolean 타입이고 value 프로퍼티는 string[] 타입인 객체 타입이 된다.
    • 따라서 값으로 해당 타입의 객체를 저장
  • 이때 주의해야 할 점
    • 제네릭 인터페이스는 제네릭 함수와는 달리 변수의 타입으로 정의할 때 반드시 꺽쇠와 함께 타입 변수에 할당할 타입을 명시해주어야 한다.
    • 제네릭 함수는 매개변수에 제공되는 값의 타입을 기준으로 타입 변수의 타입을 추론할 수 있지만 인터페이스는 마땅히 추론할 수 있는 값이 없기 때문

 

1. 인덱스 시그니쳐와 함께 사용하기

제네릭 인터페이스는 인덱스 시그니쳐와 함께 사용하면 다음과 같이 기존보다 훨씬 더 유연한 객체 타입을 정의할 수 있다.

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

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

let booleanMap: Map<boolean> = {
  key: true,
};
  • 한개의 타입 변수 V를 갖는 제네릭 인터페이스 Map을 정의
    • 이 인터페이스는 인덱스 시그니쳐로 key의 타입은 string, value의 타입은 V인 모든 객체 타입을 포함하는 타입
  • 변수 stringMap의 타입을 Map<string> 으로 정의
    • 따라서 V가 string 타입이 되어 이 변수의 타입은 key는 string이고 value는 string인 모든 프로퍼티를 포함하는 객체 타입으로 정의된다.
  • 변수 booleanMap의 타입을 Map<boolean> 으로 정의
    • 따라서 V가 boolean 타입이 되어 이 변수의 타입은 key는 string이고 value는 boolean인 모든 프로퍼티를 포함하는 객체 타입으로 정의된다.

 

2. 제네릭 타입 별칭

인터페이스와 마찬가지로 타입 별칭에도 역시 제네릭을 적용 가능

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

let stringMap2: Map2<string> = {
  key: "string",
};
  • 제네릭 타입 별칭을 사용할 때에도 제네릭 인터페이스와 마찬가지로 타입으로 정의될 때 반드시 타입 변수에 설정할 타입을 명시해 주어야 한다.

 

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

 

  • 제네릭 인터페이스 사용 전
interface Student {
  type: "student";
  school: string;
}

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

interface User {
  name: string;
  profile: Student | Developer;
}

function goToSchool(user: User<Student>) {
  if (user.profile.type !== "student") {
    console.log("잘 못 오셨습니다");
    return;
  }

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

const developerUser: User = {
  name: "이정환",
  profile: {
    type: "developer",
    skill: "typescript",
  },
};

const studentUser: User = {
  name: "홍길동",
  profile: {
    type: "student",
    school: "가톨릭대학교",
  },
};
  • 학생을 의미하는 Student와 개발자를 의미하는 Developer 타입을 정의
    • 두 타입 모두 String Literal 타입의 type 프로퍼티를 갖고 있으며, 서로소 유니온 타입이다.
  • 그리고 그 아래에 학생일수도 개발자일 수도 있는 User 타입을 정의
    • 특정 객체가 학생이라면 profile 프로퍼티에 Student 타입의 객체가 저장되고, 그렇지 않다면 Developer 타입의 객체가 저장
  • 그 아래에 학생 유저만 이용할 수 있는 함수 goToSchool을 선언
    • 이 함수에서는 일단 User 타입의 객체를 받아 타입을 좁혀 이 유저가 학생일 때에만 “등교 완료”를 콘솔에 출력한다.

❗️매번 기능을 만들기 위해 함수를 선언할 때 마다 조건문을 이용해 타입을 좁혀아 한다 → 중복코드 발생

 

💡 제네릭 인터페이스 사용하여 User 인터페이스를 제네릭 인터페이스로 업그레이드

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

interface Developer {
  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: "가톨릭대학교",
  },
};
  • goToSchool 함수의 매개변수 타입을 User<Student> 처럼 정의해 학생 유저만 이 함수의 인수로 전달하도록 제한
  • 결과적으로 함수 내부에서 타입을 좁힐 필요가 없어진다.

 


 

제네릭 클래스

  • 제네릭 클래스 사용 전
class NumberList {
  constructor(private list: number[]) {}

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

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

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

class StringList {
  constructor(private list: string[]) {}

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

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

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

const numberList = new NumberList([1, 2, 3]);
const numberList = new StringList(["1", "2", "3"]);

 

❗️타입별 리스트를 생성하는 클래스를 따로따로 정의해야한다.

 

💡제네릭 클래스를 사용해 여러 타입의 리스트를 생성할 수 있는 범용적을 클래스를 정의

  • 제네릭 클래스 사용 후
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 numberList = new List([1, 2, 3]);
const stringList = new List(["1", "2"]);

  • 클래스의 이름 뒤에 타입 변수를 선언하면 제네릭 클래스가 된다.
    • 이 타입 변수는 이제 클래스 내부에서 자유롭게 사용할 수 있다.
  • 클래스는 생성자를 통해 타입 변수의 타입을 추론할 수 있기 때문에 생성자에 인수로 전달하는 값이 있을 경우 타입 변수에 할당할 타입 생략 가능

 

💡만약 타입변수의 타입을 직접 설정하고 싶다면

class List<T> {
  constructor(private list: T[]) {}

  (...)
}

const numberList = new List<number>([1, 2, 3]);
const stringList = new List<string>(["1", "2"]);

 

 


 

Promise 사용하기

Promise는 제네릭 클래스로 구현되어 있다. 따라서 새로운 Promise를 생성할 때 다음과 같이 타입 변수에 할당할 타입을 직접 설정 → 해당 타입이 바로 resolve 결과값의 타입이 된다.

 

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

promise.then((response) => {
  // response는 number 타입
  console.log(response);
});

promise.catch((error) => {
  if (typeof error === "string") {
    console.log(error);
  }
});
  • reject 함수에 인수로 전달하는 값 즉 실패의 결과값 타입은 정의할 수 없다.
  • 그냥 unknown 타입으로 고정되어 있기 때문에 catch 메서드에서 사용하려면 타입 좁히기를 통해 안전하게 사용하는걸 권장
  • 만약 어떤 함수가 Promise 객체를 반환한다면 함수의 반환값 타입을 위해 다음과 같이 할 수 있다.
function fetchPost() {
  return new Promise<Post>((resolve, reject) => {
    setTimeout(() => {
      resolve({
        id: 1,
        title: "게시글 제목",
        content: "게시글 본문",
      });
    }, 3000);
  });
}
  • 또는 더 직관적으로 다음과 같이 반환값 타입을 직접 명시 가능
function fetchPost(): Promise<Post> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        id: 1,
        title: "게시글 제목",
        content: "게시글 본문",
      });
    }, 3000);
  });
}

'Frontend > TypeScript' 카테고리의 다른 글

[TypeScript] 8. 조건부 타입  (0) 2025.01.30
[TypeScript] 7. 타입 조작하기  (0) 2025.01.29
[TypeScript] 6-1. 제네릭이란?  (0) 2025.01.28
[TypeScript] 5. 클래스  (0) 2025.01.27
[TypeScript] 4. 인터페이스  (0) 2025.01.26