[TypeScript] Generic

dev.galim·2023년 9월 14일

TypeScript

목록 보기
8/10

Generic

→ 타입을 마치 함수의 파라미터처럼 사용하는 것.

  • 정적 타입 언어는 클래스나 함수를 정의할 때, 타입을 선언해야 한다.
  • 제네릭은 코드를 작성할 때가 아닌 코드가 수행될 때 타입을 명시한다.
  • 코드를 작성할 때 식별자를 써서 아직 정해지지 않은 타입을 표시한다.
    • 일반적으로 식별자는 T, U, V 등을 사용한다.
    • 필드 이름의 첫 글자를 사용하기도 한다.
function getText(text) {
  return text;
}

getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true
function getText<T>(text: T): T {
  return text;
}

// 함수를 호출할 때, 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있다. 
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
// getText<string>('hi'); 는 아래와 같이 타입을 정의한 것과 같다.

function getText<string>(text: string): string {
  return text;
}

제네릭을 사용하는 이유

  • 재사용성이 높은 함수와 클래스를 생성할 수 있다.
    • 여러 타입에서 동작이 가능(한번의 선언으로 다양한 타입에 재사용할 수 있음)
    • 코드의 가독성이 향상된다.
  • 오류를 쉽게 포착할 수 있다.
    • any 타입을 사용하면 컴파일 시 타입을 체크하지 않는다. 타입을 체크하지 않아 관련 메소드의 힌트를 사용할 수 없다, 컴파일 시 컴파일러가 오류를 찾지 못한다.
      function logText(text: any): any {
        return text;
      }
      • 여러 타입을 허용하고 싶어 any를 사용할 경우, 함수의 동작에 문제가 생기진 않지만 함수의 인자로 어떤 타입이 들어갔고, 어떤 타입이 반환되었는지 알 수 없다.

        any는 타입 검사를 하지 않기 때문이다.
        
        이러한 문제점을 해결할 수 있는게 바로 **Generic**이다.
        function logText<T>(text: T): T {
          return text;
        }

        → 함수의 이름 바로 뒤에 를 추가하고, 함수의 인자와 반환값에 T라는 타입을 추가하게 되면 함수를 호출할 때, 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 된다.

        따라서, 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 된다.

      • 이렇게 선언한 함수는 2가지 방법으로 호출할 수 있다.

        // #1
        const text = logText<string>("Hello Generic");
        // #2
        const text = logText("Hello Generic");
        
        /* 두번째 방법이 가독성, 코드 양 측면에서 좋기 때문에 많이 사용되다.
        복잡한 코드에서 두번째 방법으로 타입 추정이 되지 않는다면 첫번째 방법을 사용하자 */

제네릭으로 함수 만들기

function sort<T>(items: []): T[] {
	return items.sort();
}

const nums:number[] = [1, 2, 3, 4];
const chars: string[] = ["a", "b", "c", "d"];

sort<number>(nums);
sort<string>(chars); 

제네릭으로 클래스 만들기

class Queue<T> {
	protected data: Array<T> = [];
	push(item: T) {
		this.data.push(item);
	}
	pop(): T | undefined {
		return this.data.shift()
	}
}

const numberQueue = new Queue<number>();

numberQueue.push(0);
numberQueue.push("1"); // Error, number타입이 아님

Union type

|를 사용해 두 개 이상의 타입을 선언하는 것.

  • Union과 Generic 모두 여러 타입을 다룰 수 있다.
    • Union → 선언된 공통된 메소드만 사용할 수 있다. 리턴값이 하나의 타입이 아닌 선언된 Union타입으로 지정된다.
const printMsg = (msg: string | number) => {
	return msg;
}

const msg1 = printMsg(123);
const msg2 = printMsg("hello world");

msg1.length; // Error, number타입이기 때문에 에러가 발생함.
const printMsg = <T>(msg: T) => {
	return msg;
}

const msg1 = printMsg<string>("hello world");

msg1.length; // 에러가 발생하지 않음.

제약조건

→ 원하지 않는 속성에 접근하는 것을 막기 위해 제네릭에 제약조건을 사용한다.

  1. Constraints → 특정 타입들로만 동작하는 제네릭 함수를 만들 때 사용.
  2. Keyof → 두 객체를 비교할 때 사용.

Constraints

→ 제네릭 T에 제약 조건을 설명한다(문자열 or 숫자)

제약 조건을 벗어나는 타입을 선언하면 에러가 발생한다.

const printMsg = <T extends string | number>(msg: T): T => {
	return msg;
}

printMsg<Number>(123);
printMsg<String>("hello world");
printMsg<Boolean>(false); // Error, boolean 타입은 선언되어 있지 않기에 에러가 발생함.

Keyof

→ 두 객체 값을 비교할 때 사용하는 제약조건.

const getProperty = <T extends object, U extends keyof T>(obj: T, key: U) => {
	return obj[key]
}

getProperty({a: 1, b: 2, c: 3|, "a");
getProperty({a: 1, b: 2, c: 3|, "z"); // Error, "z"는 T의 키값에 존재하지 않기 때문에 에러가 발생함.

→ T는 오브젝트를 갖는 제약조건, U는 T를 갖는 제약조건을 가지고 있다.

제네릭 T는 키값이 a, b, c만 존재하는 오브젝트이기 때문에, U의 값인 ‘z’가 제네릭 T의 키 값 중 존재하지 않아 오류가 발생한다.


Factory Pattern with Generics(디자인 패턴)

→ 객체를 생성하는 인터페이스만 미리 정의하고, 인스턴스를 만들 클래스의 결정은 서브 클래스가 내리는 패턴.

  • 여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, 입력에 따라 하나의 서브 클래스의 인스턴스를 반환한다.

스크린샷 2023-09-15 오전 12.49.19.png

implements → class의 인터페이스에 만족하는지 여부를 체크할 때 사용.

extends와 implements

profile
열심히 해볼게요

0개의 댓글