티스토리 뷰

개발공부/🟦 TypeScript

[TS] Generic

2022. 11. 8. 01:46

1️⃣  Generic 이란?

 

정적 type 언어는 클래스나 함수를 정의할 때 type을 선언해야 한다.

예를 들어, C언어는 int type 변수를 선언하면 정수형 값만 할당할 수 있다.

 

Generic은 코드를 작성할 때가 아니라 코드가 수행될 때 타입을 명시한다.

(클래스나 메서드에서 사용할 내부 데이터 type을 외부에서 지정하여 정의 시점이 아니라 생성 시점에 타입을 명시할 수 있게함  →  코드의 재사용성이 높아짐)

 

코드를 작성할 때 식별자를 써서 아직 정해지지 않은 타입을 표시한다.

  • 일반적으로 식별자는 T, U, V, ...를 사용
  • 필드 이름의 첫 글자를 사용하기도 함

 

 

 

▶  Generic을 사용하는 이유

 

1)  재사용성이 높은 함수와 클래스 생성 가능

-  여러 타입에서 동작이 가능하다. (한 번의 선언으로 다양한 타입에 재사용할 수 있다.)

-  코드의 가독성이 향상된다.

 

2)  오류를 쉽게 포착 가능

-  any 타입을 사용하면 컴파일 시 타입을 체크하지 않는다  →  타입을 체크하지 않아 관련 메소드의 힌트를 사용할 수 없다  →  컴파일 시에 컴파일러가 오류를 찾지 못한다

-  Generic도 any처럼 미리 타입을 지정하지는 않지만, 타입을 체크해 컴파일러가 컴파일 시점에서 오류를 찾을 수 있다.

 

 

→  따라서, 타입스크립트에서는any타입을 지양하고, 다양한 타입 지정이 필요할 때는 generic 또는 union type을 권장한다.

 

 

 

 

 

 

2️⃣  Generic으로 함수와 클래스 만들기

 

generic을 활용한 함수

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

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

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

 

 

generic을 활용한 class

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 발생 -> 의도하지 않은 실수를 사전 검출 가능

 

 

 

 

 

 

3️⃣  Union type

 

"|"를 사용해 두 개 이상의 타입을 선언하는 방식

 

Union과 Generic 모두 여러 타입을 다룰 수 있다.

  • Union은 선언한 공통된 메소드만 사용 가능
  • 리턴 값이 하나의 타입이 아닌 선언된 Union 타입으로 지정

 

const printMessage = (message: string | number) => {
    return message;
}

const message1 = printMessage(1234);
const message2 = printMessage("hello world !");

message1.length;    // error: length does not exist on type string | number
                    // -> string과 number type의 공통된 메소드만 사용 가능하다

위 코드를 generic으로 쓴다면

const printMessage2 = <T>(message: T) => {
    return message;
}

const message1 = printMessage2<string>("hello world !");
message1.length;    // 위 union type 사용 시와 달리 error 발생하지 않음

 

 

 

 

 

 

 

4️⃣  제약조건 (Constraints / keyof)

 

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

extends키워드를 사용하여 특정 타입들로만 동작하는 제네릭 함수를 만들고 싶을 때 사용한다.

 

 

 

1)  Constraints 

:  특정 타입들로만 동작하는 Generic 함수를 만들 때 사용

 

-  Generic T에 제약 조건을 설정한다. (문자열 or 숫자)

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

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

printMessage<string>("1");
printMessage<number>(1);
printMessage<boolean>(false)    // Error: Type 'boolean' does not satisfy the constraint 'string | number'.

 

 

 

 

2)  Keyof 

:  두 객체를 비교할 때 사용한다.

 

-  Generic T는 키 값이 a, b, c만 존재하는 object이다.

-  U의 값인 'z'가 Generic T의 키 값 중 존재하지 않기 때문에 오류가 발생한다.

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: Argument of type '"z"' is not assignable to parameter of type '"a" | "b" |“c"'

 

 

 

 

 

 

 

5️⃣  디자인 패턴 (Factory Pattern with Generics)

 

객체를 생성하는 인터페이스는 미리 정의하되, 인스턴스를 만들 클래스의 결정은 서브 클래스에서 진행하는 패턴

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

 

 

Factory Pattern

interface Car {
    drive(): void
    park(): void
}

class Bus implements Car {
    drive(): void {}
    park(): void {}
}

class Taxi implements Car {
    drive(): void {}
    park(): void {}
}

class CarFactory {    // 실제로 객체를 생성시키는 클래스
    static getInstance(type: string): Car {    // 전역멤버로 구현, 생성시키고자 하는 클래스를 인수값(type)으로 전달
        switch (type) {
            case "bus":
                return new Bus();
            default:
                return new Taxi();
        }
    }
}

const bus = CarFactory.getInstance("bus");
const taxi = CarFactory.getInstance("taxi");

위 코드는 car의 type이 추가 될 때마다 case 문을 추가 해야하는 단점이 있다.

 

 

위 코드를 Generics을 활용한 Factory Pattern으로 바꾸면 아래와 같다.

interface Car {
    drive(): void
    park(): void
}

class Bus implements Car {
    drive(): void {}
    park(): void {}
}

class Taxi implements Car {
    drive(): void {}
    park(): void {}
}

class Suv implements Car {
    drive(): void {}
    park(): void {}
}

export class CarFactory {
    static getInstance<T extends Car>(type: { new (): T }): T {
        return new type();
    }
}

const bus = CarFactory.getInstance(Bus);
const taxi = CarFactory.getInstance(Taxi);

 

 

 

 

 


 이 글은 엘리스의 AI트랙 5기 강의를 들으며 정리한 내용입니다.

반응형
프로필사진
개발자 삐롱히

프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그