Jieunny의 블로그

Section 7. 제네릭 본문

CodeStates/TS 스터디

Section 7. 제네릭

Jieunny 2023. 2. 22. 11:11

📣  강의 내용 정리

𝟭.  내장 제네릭 & 제네릭이란?

✔️ 제네릭 타입 : 타입을 마치 함수의 파라미터처럼 사용하는 것

➰ 추가적인 타입 정보를 얻는 데 도움이 된다.

제네릭 타입을 사용하는 경우에는 입력되는 데이터의 타입을 지정해주어야 한다.

const names: Array<string> = []; // string[]과 같은 의미

 

𝟮.  제네릭 함수 생성하기

✔️ 함수의 매개변수, 반환 타입에도 제네릭을 사용할 수 있다.

function merge(objA: object, objB: object) {
    return Object.assign(objA, objB);
}

console.log(merge({name: 'Max'}, {age: 30});
// {name: 'Max', age: 30}

const mergedObj = merge({name: 'Max'}, {age: 30}) 
mergedObj.age;	// age에 접근할 수 없다 -> 타입스크립트는 merge함수가 객체를 반환하는 것으로 추론하기 떄문
// 'object 형식에 age속성이 없습니다' 에러
// 사용할 수 있는 모든 정보를 담고 있지는 않다. 

const mergedObj = merge({name: 'Max'}, {age: 30}) as {name: string, age: number}
// 이렇게 하면 가능하지만 아주 번거로움.


// 제네릭 타입
function merge<T, U>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}
const mergedObj = merge({name: 'Max'}, {age: 30}) 
console.log(mergedObj.age) // 30
// const mergedObj: {name: string} & {age: number} 로 추론한다.
// 정확히 어떤 타입이 될 지는 모른다는 추가 정보를 타입스크립트에게 제공하는 것

 

𝟯.  제약 조건 작성하기

✔️ extends를 사용해서 들어오는 타입을 명확히 할 수 있다.

function merge<T extends object, U extends object>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}

const mergedObj = merge({name: 'Max'}, {age: 30}) 
console.log(mergedObj.name) // Max
// 이렇게 하면 속성에 접근 가능

➰ 제네릭 타입에 특정한 제약 조건을 설정함으로써 두 '객체'만을 얻을 수 있도록 한다.

 

𝟰.  다른 일반 함수

interface Lengthy {
    length: number;
}

function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
// 첫번째 요소는 제네릭타입, 두번째 요소는 문자열이 되는 튜플을 반환
    let descriptionText = 'Got no value.';
    if(element.length === 1) descriptionText = 'Got 1 element.';
    // 그냥 <T>로 하면 타입스크립트는 element가 지닌 length가 불명확하다고 문제를 나타낸다.
    // Lengthy를 추가해주면 에러가 없어진다.
    else if(element.length > 1) dedcriptionText = 'Got' + element.length + 'elements.';
    return [element, descriptionText];
}

length 속성을 지니는 타입만 입력할 수 있다.

➰ T extends Lengthy라는 제약조건을 걸어, 이로써 얻는 것이 무엇이든 length속성을 지닌다는 것을 타입스크립트에게 알려준다.

 

𝟱.  keyof 제약조건

function extractAndConvert(obj: object, key: string) {
    return obj[key];
}

입력한 객체가 무엇이든, 저 key를 가지는지 알 수 없기 때문에 에러가 발생한다.

 

function extractAndConvert<T extends object, U extends keyof T>(obj: T,key: U) {
    return 'Value: ' + obj[key];
}
// T 객체가 가지고 있는 속성만 입력할 수 있게 제한한다.

extractAndConvert({ name: 'Max' }, 'name');
// 매개변수 객체에 name 속성이 없으면 에러가 발생한다.

➰ keyof 키워드를 지니는 제네릭 타입을 사용해서 타입스크립트에게 정확한 구조를 알려준다.

➰ keyof : Object의 key들의 lieteral 값들을 가져온다

 

𝟲.  제네릭 클래스

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(10); //error -> string만 저장하는 dataStorage를 입력했기 때문이다.
textStorage.addItem('Max'); 
textStorage.addItem('Manu'); 
textStorage.removeItem('Max');
console.log(textStorage.getItems());
//['Manu']

 

const objStorage = new DataStorage<object>();
const maxObj = {name: 'Max'};
objStorage.addItem(maxObj);
objStorage.addItem({name: 'Manu'});
objStorage.removeItem({name: 'Max'});
// 이렇게 하면 name: 'Max' 객체가 삭제되지 않는다.
objStorage.removeItem(maxObj);
// 정확히 같은 객체를 전달한다.
console.log(objStorage.getItems());

➰ T에 object 타입을 주면 코드가 제대로 작동하지 않는다.

➰ 객체는 참조 자료형이기 때문에 주소값을 바탕으로 동작하기 때문에 삭제할 아이템을 찾아서 삭제하는 과정이 제대로 이루어지지 않는다 -> maxObj와 {name: 'Max} 는 주소값이 다르기 때문에 아예 다른 객체!

 객체가 제대로 작동하게 하려면 정확히 같은 객체를 다시 전달해야 한다.

 

𝟳.  제네릭 유틸리티 타입(TS에서만 존재)

✔️ Partial : 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.

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

function createCourseGoal(
    title: string;
    desctription: string,
    date: Date
): CourseGoal {
    let courseGoal: Partial<CourseGoal> = {};
    // 속성을 부분적으로 갖게 하는 Partial를 사용해서 빈 객체도 할당할 수 있게 한다.
    // CourseGoal의 부분집합에는 빈 객체도 있기 때문에
    couseGoal.title = title;
    courseGoal.description = description;
    courseGoal.completeUntil = date;
    return courseGoal as courseGoal;
    // 파셜 타입은 CourseGoal의 파셜 타입이지 CourseGoal이 아니기 때문에 형 변환해서 반환해야 한다.
}

// createCourseGoal 함수는 3개의 인자를 받고 CourseGoal 타입을 반환한다.

 

✔️ Readonly : 읽기만 가능한 타입

const names: Readonly<string[]> = ['Max', 'Anna'];
names.push('Manu'); // 에러

 

𝟴.  제네릭 타입 vs 유니언 타입

Q. 유니언 타입을 사용해서 여러 타입을 정의할 수 있는데 왜 제네릭을 사용할까?

 

✔️ 유니언 타입 

➰ 문자열, 숫자, 불리언의 배열이 아니라 세 타입이 혼합된 배열이라고 입력된다.

(한 배열에 문자열, 숫자, 불리언이 혼합되어 들어갈 수 있다는 의미)

➰ 함수를 호출할 때 마다 이 타입들 중 하나로 호출할 수 있는 함수가 필요한 경우에 유용하다.

class DataStorage {
    private data: (string | number | boolean)[] = [];
    //private data: (string[] | number[] | boolean[])[] = [];
    //이 주석의 코드가 각각 다른 배열이 들어올 수 있다는 뜻
    
    addItem(item: string | number | boolean) {
        this.data.push(item);
    }
}

 

✔️ 제네릭 타입

 저장하려는 데이터의 타입을 '하나' 선택해야 하고, 그 다음에는 '해당 타입의 데이터'만 추가할 수 있다. 

➰ 특정 타입을 고정하거나, 생성한 전체 클래스 인스턴스에 걸쳐 같은 함수를 사용하거나, 전체 함수에 걸쳐 같은 타입을 사용하고자 할 때 유용하다.

class DataStorage<T> {
    private data: T[] = [];
    
    addItem(item: T) {
        this.data.push(item);
    }
}

 

📚 참고 : https://joshua1988.github.io/ts/intro.html


📣  새로 알게 된 점

1. keyof 제약 조건

2. 원래 알고 있던 내용 복습

 

💡 강의에서 조금 어렵게 설명하는 느낌이 있어서 타입스크립트 핸드북을 참고하면서 진행하면 좋을 것 같다.