TypeScript / / 2024. 10. 29. 21:50

TypeScript 제네릭2

제네릭 인터페이스, 타입 별칭, 그리고 활용 예시

**제네릭(Generic)**은 타입을 일반화하여 여러 타입에 대해 유연하게 작동할 수 있는 코드를 작성하는 강력한 도구입니다. 이를 인터페이스, 타입 별칭, 그리고 실용적인 예시에 적용해 봅시다.

1. 제네릭 인터페이스

제네릭 인터페이스는 특정 인터페이스가 여러 타입을 받을 수 있도록 만들어 주는 기능입니다. 예를 들어, **KeyPair**라는 제네릭 인터페이스를 정의하여 키와 값을 서로 다른 타입으로 설정할 수 있습니다.

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

여기서 **K**와 **V**는 각각 key value의 타입을 정의하는 타입 변수입니다.

  • 사용 예:
  • let keyPair: KeyPair<string, number> = {
      key: "key",
      value: 0,
    };
    
    let keyPair2: KeyPair<boolean, string[]> = {
      key: true,
      value: ["1"],
    };
  • keyPair의 타입은 KeyPair<string, number>로 설정되어, key는 string 타입이고 value는 number 타입이 됩니다.
  • keyPair2의 타입은 KeyPair<boolean, string[]>로 설정되어, key는 boolean 타입이고 value는 string[] 타입입니다.

주의점: 제네릭 인터페이스를 사용할 때는 항상 타입 변수를 명시해 주어야 합니다. 이는 함수와 달리 인터페이스는 타입을 추론할 기준이 없기 때문입니다.

2. 인덱스 시그니처와 제네릭

인덱스 시그니처와 제네릭을 함께 사용하면 객체 타입을 더 유연하게 정의할 수 있습니다.

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

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

let booleanMap: Map<boolean> = {
  key: true,
};
  • 여기서 Map<V> 인터페이스는 [key: string]: V 형태로 정의되어, 모든 키는 문자열이고 **값은 타입 변수 V**에 의해 결정됩니다.
  • stringMap은 Map<string>으로 정의되어  string 타입인 객체를 뜻합니다.
  • booleanMap은 Map<boolean>으로 정의되어  boolean 타입인 객체를 뜻합니다.

이렇게 하면 어떤 키-값 쌍이든 유연하게 표현할 수 있는 객체 타입을 정의할 수 있습니다.

3. 제네릭 타입 별칭

타입 별칭에도 제네릭을 적용할 수 있습니다. 타입 별칭을 사용하면 타입에 별명을 붙여서 재사용할 수 있습니다.

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

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

여기서 **Map2<V>**는 Map 인터페이스와 같은 형태로 인덱스 시그니처를 포함합니다. 값의 타입을 지정하면 그 타입으로 객체를 정의할 수 있습니다.

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

아래는 제네릭 인터페이스의 실용적인 활용 예시입니다. **학생(Student)**과 개발자(Developer) 타입을 정의하고 이를 **사용자(User)**에 연결해봅니다.

1) 학생과 개발자 타입 정의

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

interface Developer {
  type: "developer";
  skill: string;
}
  • Student는 학생을 나타내며 type과 school이라는 속성을 가집니다.
  • Developer는 개발자를 나타내며 type과 skill 속성을 가집니다.

2) User 타입에 제네릭 적용하기

제네릭을 이용해 User 타입을 유연하게 정의합니다.

interface User<T> {
  name: string;
  profile: T;
}
  • User<T>는 제네릭 인터페이스로, T에 학생 또는 개발자 같은 타입을 받을 수 있습니다.

3) 사용자 정의 및 함수 활용

학생과 개발자 객체를 정의하고, 특정 사용자만 사용할 수 있는 함수를 만듭니다.

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(studentUser);  // "가톨릭대학교로 등교 완료"​
 
  • goToSchool 함수는 User<Student> 타입의 객체만 받을 수 있도록 정의되어 있습니다.
  • 이를 통해 학생 유저만 이 함수의 인수로 전달할 수 있어, 타입 안전성을 높이고 불필요한 조건문을 줄일 수 있습니다.

요약

  1. 제네릭 인터페이스:
    • 인터페이스에 제네릭을 적용하여 여러 타입을 지원하는 인터페이스를 정의할 수 있습니다.
    • 예: interface KeyPair<K, V> { key: K; value: V; }
  2. 인덱스 시그니처와 제네릭:
    • 인덱스 시그니처와 제네릭을 결합하여 유연한 객체 타입을 정의할 수 있습니다.
    • 예: interface Map<V> { [key: string]: V; }
  3. 제네릭 타입 별칭:
    • 타입 별칭에도 제네릭을 적용하여 유연하게 사용할 수 있습니다.
    • 예: type Map2<V> = { [key: string]: V; }
  4. 제네릭 인터페이스 활용:
    • **유저(User)**와 같은 인터페이스에 제네릭을 사용하면 특정 타입의 사용자(학생 또는 개발자 등)에 대해 타입 안전성을 높일 수 있습니다.
    • 특정 유저 타입만 인수로 받는 함수에서 타입을 좁히는 작업 없이 직접 활용할 수 있습니다.

제네릭을 사용하면 타입의 유연성 코드의 재사용성을 동시에 가져갈 수 있습니다. 특히, 제네릭 인터페이스를 활용하면 코드의 안정성이 강화되고 중복 코드를 줄일 수 있어, 실용적이고 효율적인 코드를 작성할 수 있습니다.

 

제네릭 클래스 (Generic Class)

제네릭 클래스는 다양한 타입에 대해 하나의 클래스로 작업을 수행할 수 있도록 만든 범용적인 클래스입니다. 이를 통해 중복 코드를 줄이고, 코드의 재사용성 유연성을 높일 수 있습니다. 아래에서는 제네릭 클래스의 개념과 그 적용 방법을 살펴보겠습니다.

1. 제네릭이 없는 클래스의 문제점

먼저, 제네릭이 적용되지 않은 NumberList 클래스를 정의해 봅니다.

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 클래스 list 필드를 가지고 있으며, 이를 통해 숫자 요소를 추가(push), 제거(pop), 출력(print)할 수 있습니다.
  • list 필드는 private으로 설정되어 있어, 클래스 내부에서만 접근이 가능합니다.

이제, 같은 방식으로 **문자열 리스트(StringList)**를 필요로 한다면 새로운 클래스를 정의해야 합니다.

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

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

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

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

const stringList = new StringList(["1", "2", "3"]);
  • NumberList StringList 클래스는 거의 동일한 구조를 가집니다.
  • 다른 타입의 리스트를 만들기 위해 중복 코드를 작성해야 하므로, 유지보수가 어렵고 효율적이지 않다는 문제가 발생합니다.

2. 제네릭 클래스 사용하기

위의 중복 문제를 해결하기 위해 제네릭 클래스를 사용할 수 있습니다. 제네릭을 사용하면 하나의 클래스로 여러 타입의 리스트를 처리할 수 있게 됩니다.

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"]);
  • 제네릭 클래스 선언:
    • 클래스 이름 뒤에 **<T>**를 사용해 **타입 변수 T**를 선언합니다.
    • 이제 클래스 내부에서 **T**를 사용할 수 있으며, 리스트의 타입 T로 일반화했습니다.
    • 따라서 **T**는 숫자, 문자열, 기타 다른 타입으로 사용할 수 있습니다.
  • 사용 예:
    • new List([1, 2, 3]): **T**는 **number**로 추론됩니다.
    • new List(["1", "2"]): **T**는 **string**으로 추론됩니다.

3. 타입 직접 명시하기

타입스크립트는 일반적으로 생성자에 전달된 값을 기준으로 타입을 추론하지만, 명시적으로 타입을 지정할 수도 있습니다.

const numberList = new List<number>([1, 2, 3]);
const stringList = new List<string>(["1", "2"]);
  • <number>, **<string>**과 같이 타입을 직접 명시할 수 있습니다.
  • 이렇게 하면 타입 안정성이 더욱 명확해지며, 타입 오류를 방지할 수 있습니다.

4. 제네릭 클래스의 장점 요약

  1. 타입의 유연성:
    • 제네릭 클래스를 사용하면 다양한 타입의 리스트를 하나의 클래스로 처리할 수 있습니다. 숫자, 문자열, 객체 등 여러 타입을 처리할 수 있어 유연성이 높아집니다.
  2. 중복 코드 제거:
    • 같은 기능을 하는 클래스를 타입만 다르게 여러 개 작성할 필요가 없어지므로 중복 코드가 제거됩니다. 이는 유지보수에 있어서도 큰 장점입니다.
  3. 타입 안정성:
    • 제네릭을 사용함으로써 타입스크립트의 타입 추론 기능을 그대로 사용할 수 있어 타입 안정성을 보장합니다. 이를 통해 잘못된 타입의 데이터를 처리하는 오류를 줄일 수 있습니다.

5. 제네릭 클래스 사용 예시

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<number>([1, 2, 3]);  // number 타입 리스트
numberList.push(4);
numberList.print();  // 출력: [1, 2, 3, 4]

const stringList = new List<string>(["a", "b", "c"]);  // string 타입 리스트
stringList.push("d");
stringList.print();  // 출력: ["a", "b", "c", "d"]
  • 숫자 리스트 (numberList)와 문자열 리스트 (stringList)를 하나의 제네릭 클래스로 관리할 수 있습니다.
  • push, pop, print 등의 메서드가 타입에 맞게 동작하며, 타입 오류를 방지할 수 있습니다.

요약

  • 제네릭 클래스는 다양한 타입을 하나의 클래스로 처리할 수 있도록 해 주는 강력한 기능입니다.
  • 제네릭을 사용하면 중복 코드가 제거되고, 유지보수가 쉬운 코드를 작성할 수 있습니다.
  • 클래스의 이름 뒤에 **타입 변수 T**를 선언하여, 클래스 내부에서 모든 타입 관련 작업을 T로 일반화할 수 있습니다.
  • 제네릭 클래스를 사용하면 코드의 유연성, 재사용성, 타입 안전성이 모두 향상됩니다.

제네릭 클래스를 사용함으로써, 코드의 유연성과 안정성을 모두 확보할 수 있으며, 이를 통해 유지보수하기 쉬운 코드 구조를 만들 수 있습니다. 특히 리스트와 같은 데이터 구조를 구현할 때 매우 유용하며, 여러 타입을 다루는 작업에서도 복잡성을 줄일 수 있는 장점이 있습니다.

 

Promise 사용하기

Promise 비동기 작업의 완료나 실패를 나타내는 객체입니다. Promise는 타입스크립트에서 제네릭(Generic) 클래스로 구현되어 있으며, 이를 통해 resolve 결과값의 타입을 명확히 정의할 수 있습니다.

아래에서는 Promise의 기본 사용법 타입 정의에 대해 살펴보겠습니다.

1. Promise 기본 사용법

Promise는 비동기 작업이 성공했을 때 호출되는 **resolve**와, 실패했을 때 호출되는 **reject**를 사용하여 결과를 처리합니다. 제네릭 타입을 사용해 resolve 값의 타입을 명확하게 설정할 수 있습니다.

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

promise.then((response) => {
  // response는 number 타입으로 추론됨
  console.log(response);  // 출력: 20
});

promise.catch((error) => {
  if (typeof error === "string") {
    console.log(error);
  }
});
  • 제네릭 타입 설정:
    • **new Promise<number>**를 사용해 number 타입의 결과값을 가진 Promise를 생성합니다.
    • **resolve(20)**은 성공 시 반환될 값이며, 이 값의 타입은 **number**입니다.
  • then 메서드:
    • 비동기 작업이 성공했을 때 호출됩니다.
    • response는 **resolve**에서 넘겨받은 **20**으로, 타입스크립트는 이를 **number**로 추론합니다.
  • catch 메서드:
    • 비동기 작업이 실패했을 때 호출됩니다.
    • reject 함수의 인수로 전달된 실패 이유의 타입은 기본적으로 **unknown**입니다.
    • 따라서 안전하게 사용하려면 타입 좁히기(typeof)를 통해 타입을 확인하고 사용해야 합니다.

주의사항: reject 함수로 전달하는 실패 이유의 타입은 타입 변수로 설정할 수 없고, 기본적으로 unknown 타입입니다. 따라서 실패 값을 사용할 때는 항상 타입을 좁혀서 사용해야 합니다.

2. Promise를 반환하는 함수

Promise를 반환하는 함수에서는 반환값의 타입을 명확하게 지정할 수 있습니다. 이를 통해 함수가 반환하는 Promise의 resolve 타입을 명확히 정의하고 타입 안정성을 유지할 수 있습니다.

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

// 방법 1: Promise 생성 시 타입 지정
function fetchPost() {
  return new Promise<Post>((resolve, reject) => {
    setTimeout(() => {
      resolve({
        id: 1,
        title: "게시글 제목",
        content: "게시글 본문",
      });
    }, 3000);
  });
}
  • fetchPost 함수는 비동기 작업을 시뮬레이션하기 위해 Promise 객체를 반환합니다.
  • Post 인터페이스는 게시글 데이터를 나타내며, id, title, content의 속성을 가집니다.
  • **resolve**를 통해 Post 타입의 객체를 반환합니다.
function fetchPost(): Promise<Post> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        id: 1,
        title: "게시글 제목",
        content: "게시글 본문",
      });
    }, 3000);
  });
}
  • 반환 타입 명시:
    • 함수의 반환 타입을 **Promise<Post>**로 명시합니다.
    • 이 경우, 타입스크립트는 함수가 Post 타입의 값을 반환하는 Promise임을 인식합니다.
  • 이 방식으로 반환 타입을 명확히 정의하면, 함수가 반환하는 Promise의 resolve 타입을 더욱 명확하게 나타낼 수 있어 타입 안정성이 향상됩니다.

요약

  1. Promise의 제네릭 타입:
    • **new Promise<number>**와 같이 제네릭 타입을 사용하여 Promise의 resolve 값의 타입을 명확하게 정의할 수 있습니다.
    • 이를 통해 then 메서드에서 사용되는 값의 타입이 자동으로 추론되므로 타입 안전성을 유지할 수 있습니다.
  2. reject 값의 타입:
    • reject 함수에 전달되는 실패 이유의 타입은 **unknown**으로 고정됩니다.
    • 따라서 catch 메서드에서 이 값을 사용하려면 타입 좁히기(typeof 등을 사용)를 통해 안전하게 처리하는 것이 좋습니다.
  3. Promise를 반환하는 함수의 타입 명시:
    • 비동기 작업을 수행하는 함수가 Promise 객체를 반환하는 경우, 함수의 반환 타입을 명확하게 명시하는 것이 좋습니다.
    • 예를 들어, **function fetchPost(): Promise<Post>**와 같이 반환 타입을 명확히 정의하면 타입 안정성이 향상됩니다.

실용적인 예시와 장점

  • 비동기 작업 타입 안정성: 제네릭을 사용하여 Promise의 결과값의 타입을 명확히 지정하면, 타입스크립트의 타입 추론 기능을 활용하여 비동기 작업의 타입 안전성을 보장할 수 있습니다.
  • 코드의 가독성과 유지보수성: 반환 타입을 명확히 지정하면 코드의 가독성이 높아지고, 유지보수 시 실수할 가능성이 줄어듭니다.

이렇게 Promise를 제네릭과 함께 사용하면 비동기 작업의 타입 안정성을 유지하면서 다양한 비동기 처리를 안전하게 관리할 수 있습니다. 이는 코드의 가독성을 높이고, 오류를 사전에 방지하는 데 매우 유용합니다.

'TypeScript' 카테고리의 다른 글

TypeScript 조건부 타입  (1) 2024.11.03
TypeScript 타입조작하기  (0) 2024.11.02
TypeScript 제네릭1  (1) 2024.10.29
TypeScript 인터페이스로 구현하는 클래스  (0) 2024.10.29
TypeScript 클래스  (1) 2024.10.29
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유