본문 바로가기

Frontend/Typescript

제네릭

제네릭

/**
 * 제네릭
 */

// 제네릭 함수
function func<T>(value: T): T {
    return value;
}

let num = func(2);
let bool = func(true);
let str = func('string');
let arr = func<[number, number, number]>([1, 2, 3]);  // 튜플

위 함수에서 반환값의 타입은 함수 호출 시 결정된다.

튜플 타입을 넣어주고 싶은 경우, 타입 단언을 사용하는 것보다 위와 같이 나타내는 것이 더 좋다.

 

타입 변수 응용하기

/**
 * 첫 번째 사례
 */

function swap<T, U>(a: T, b: U): [U, T] {
    return [b, a];
}

const [a, b] = swap('1', 2);

/**
 * 두 번째 사례
 */

function returnFirstValue1<T>(data: T[]): T {
    return data[0];
}

let num1 = returnFirstValue1([1, 'hello', 'hi']);  // string | number 타입

function returnFirstValue2<T>(data: [T, ...unknown[]]): T {
    return data[0];
}

let num2 = returnFirstValue2([1, 'hello', 'hi']);  // number 타입

/**
 * 세 번째 사례
 */

function getLength<T extends { length: number }>(data: T) {
    return data.length;
}

let var1 = getLength([1, 2, 3]);
let var2 = getLength({ length: 10 });
let var3 = getLength('1234');

세 번째 사례의 경우, extends 키워드를 사용해서 T의 범위를 제한하는 것이다.

 

map, forEach 메서드 타입 정의하기

/**
 * map 메서드
 */

const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2);

function map<T, U>(arr: T[], callback: (item: T) => U) {
    let result = [];

    for (let i = 0; i < arr.length; i++) {
        result.push(callback(arr[i]));
    }

    return result;
}

map(arr, (it) => it * 2);
map(['hi', 'hello'], (it) => parseInt(it));

/**
 * forEach 메서드
 */

const arr2 = [1, 2, 3];
arr2.forEach((it) => console.log(it));

function forEach<T>(arr: T[], callback: (item: T) => void) {
    for (let i = 0; i < arr.length; i++) {
        callback(arr[i]);
    }
}

forEach(arr2, (it) => console.log(it.toFixed()));

 

제네릭 인터페이스, 제네릭 타입 별칭

/**
 * 제네릭 인터페이스
 */

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

let keyPair1: KeyPair<string, number> = {
    key: 'key',
    value: 0
}

let keyPair2: KeyPair<boolean, string[]> = {
    key: true,
    value: ['1']
}

/**
 * 인덱스 시그니처
 */

interface NumberMap {
    [key: string]: number;
}

let numberMap1: NumberMap = {
    key1: -123,
    key2: 23,
}

interface Map1<V> {
    [key: string]: V
}

let stringMap: Map1<number> = {
    key1: 123,
    key2: 345
}

/**
 * 제네릭 타입 별칭
 */

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

let stringMap2: Map2<string> = {
    key1: 'hello',
    key2: 'hi'
}

/**
 * 제네릭 인터페이스의 활용 예시
 */

interface Student {
    type: 'student',
    school: string;
}

interface Developer {
    type: 'developer',
    skill: string;
}

interface User<T> {
    name: string;
    profile: T;
}

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 같은 함수에서 제네릭 인터페이스를 사용하지 않고 인터페이스의 유니온을 사용해서 매개변수로 User를 받는 경우에는 타입 가드를 해주어야 한다.

하지만, 제네릭 인터페이스를 사용해서 매개변수로 User<Student>로 받는 경우에는 User<Developer>의 경우를 고려해줄 필요가 없게 된다.

 

제네릭 클래스

/**
 * 제네릭 클래스
 */

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, 4]); 

numberList.pop();
numberList.push(5);
numberList.print();

제네릭 클래스의 경우, 클래스의 생성자 호출 시 인수로 전달하는 값으로 타입을 추론하게 된다.

따라서, 제네릭 타입 별칭과 제네릭 인터페이스와는 다르게 타입을 별도로 명시해주지 않아도 된다.

 

프로미스와 제네릭

/**
 * 프로미스
 */

const promise = new Promise<number>((resolve, reject) => { 
    setTimeout(() => {
        //resolve(20);
        reject('실패 원인 ...');
    }, 3000);
});

promise.then((response) => {
    console.log(response * 10);
})

promise.catch((err) => {
    if (typeof err === 'string') {
        console.log(err);
    }
})

/**
 * 프로미스를 반환하는 함수의 타입을 정의
 */

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

function fetchPost(): Promise<Post> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({
                id: 1,
                title: '1',
                content: '1'
            })
        }, 3000);
    })
}

const postRequest = fetchPost();

postRequest.then((post) => {
    post.id
});

resolve의 경우, 타입을 기본적으로는 unknown 타입으로 추론한다.

이때 제네릭을 사용하면 문제를 해결할 수 있다.

위 코드와 같이 프로미스 결과값의 타입을 지정해주면 된다.

'Frontend > Typescript' 카테고리의 다른 글

조건부 타입과 유틸리티 타입  (0) 2023.06.23
타입 조작하기  (0) 2023.06.23
인터페이스와 클래스  (0) 2023.06.20
함수와 타입  (0) 2023.06.20
타입스크립트 이해하기  (0) 2023.06.20