(TS) Generics 2️⃣

  • by


🍒 (230317 TIL) Understanding TypeScript

Generics 2️⃣

Keyof Constraint

제네릭으로 객체의 제약을 주는 경우, 키치가 필요한 경우 keyof를 사용한 제약을 사용하면 실수를 방지할 수 있다.

function extractAndConvert<T extends object, U extends keyof T>(
    obj: T,
    key: U
) {
    return 'Value: ' + obj(key);
}

extractAndConvert({}, 'name'); // Error occurred

제네릭 T의 제약으로 object설정하고,

U의 제약으로 Tkey설정했습니다.

extractAndConvert로빈 객체({})와 name라는 문자열을 전달할 때,

빈 객체에 name라는 키 값이 없기 때문에 컴파일 에러가 나온다!

제네릭으로 U extends keyof T설정했기 때문에 발생하는 오류이며,

함수로서 name 키를 가진 객체를 전달하면 오류가 사라집니다 😃

Generic Classes

제네릭으로 클래스를 작성하는 경우, 새로운 인스턴스를 작성할 때에 유연하게 형태를 변경할 수가 있다.

class DataStorage<T> {
    private data: T() = ();
    
    addItem(item: T) {
        this.data.push(item);
    }
    
    removeItem(item: T) {
        this.data.splice(this.data.indexOf(item), 1);
    }
    
    getItems() {
        return (...this.data);
    }
}

const textStorage = new DataStorage<string>();
textStorage.addItem('Apeach');
textStorage.addItem('Rabbit');
textStorage.removeItem('Apeach');
console.log(textStorage.getItems()); // ('Rabbit')

위의 예에서 제네릭을 사용하지 않는 경우 class 직접 유형을 지정한 경우 string | number 등…

필요한 유형을 직접 추가해야하는 번거로움이 있었을 것입니다 🤔

선언 시점이 아닌 생성 시점에 필요한 유형을 지정할 수 있습니다.

라는 것은 제네릭의 강대한 장점!

물론 제약을 제네릭에 추가하는 방법도 있다 😅

Partial type

Partial 형식은 제네릭에 전달된 형식이 개체임을 알리는 역할을 하면서

건네받은 형태로 작성된 모든 속성을 옵션형으로 변경합니다.

interface CourseGoal {
    title: string;
    description: string;
    completeUntil: Date;
}

function createCourseGoal(
    title: string,
    description: string,
    date: Date
): CourseGoal {
    let courseGoal: Partial<CourseGoal> = {};
    courseGoal.title = title;
    courseGoal.description = description;
    courseGoal.completeUntil = date;
    return courseGoal as CourseGoal;
}

Readonly type

Readonly 타입은 제네릭에게 전달된 타입이 무조건 읽기만 가능하다는 것을 알려주는 역할을 한다.

Readonly 타입을 사용하면 속성의 변경이나 추가가 불가능합니다 😵

const names: Readonly<string()> = ('Shou', 'Apeach');
names.push('Rabbit'); // Error occurred
names.pop(); // Error occurred

Utility Types
https://www.typescriptlang.org/docs/handbook/utility-types.html

Generics vs Union types

class DataStorage<T> {
    private data: T() = ();
    
    addItem(item: T) {
        this.data.push(item);
    }
    
    removeItem(item: T) {
        this.data.splice(this.data.indexOf(item), 1);
    }
    
    getItems() {
        return (...this.data);
    }
}

const numberStorage = new DataStorage<number>();

제네릭의 경우 인스턴스를 만들 때 number 형태의 배열을 형태로 지정하면 지정할 수 있습니다만,

class DataStorage {
    private data: (string | number | boolean)() = ();
    
    addItem(item: string | number | boolean) {
        this.data.push(item);
    }
    
    removeItem(item: string | number | boolean) {
        this.data.splice(this.data.indexOf(item), 1);
    }
    
    getItems() {
        return (...this.data);
    }
}

union 유형은 data라는 배열에 any 유형을 지정한 것과 다르지 않습니다 ..🫢 (예는 실제로 any처럼 작동하지 않지만)

이 경우 data유형 string() | number() | boolean()로 설정할 수 있지만,

이렇게 되면 data를 어떤 타입의 배열로 설정하는가에 따라 item의 타입도 유동적으로 바뀌어야 하기 때문에 item의 타입 설정이 어려워진다😡

제네릭은 특정 유형을 고정하거나 생성된 클래스 전체 인스턴스 전체에서 동일한 함수를 사용하거나 함수 전체에서 동일한 유형을 사용할 때 유용합니다.


union 유형은 모든 메소드 호출과 모든 함수 호출마다 다른 유형을 지정하려는 경우에 유용합니다.