ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] Promise에서 Generic과 제네릭의 제약 조건
    Front-end 개발 2024. 2. 16. 22:26

    TypeScript

     

    1. 타입스크립트 제네릭(Generic) 문법


    '포괄적인'의 뜻을 가지고 있는 제네릭(Generic)은 타입스크립트 제네릭 문법은 함수를 시작으로 이해하면 효과적이다.

    제네릭 (Generic): (형용사) 포괄적인, 총칭[통칭]의

     

    TypeScript의 제네릭을 함수에 적용하면, 함수의 타입 변경 없이도 매개변수로 전달되는 타입이 반환값의 타입이 되도록 할 수 있다. 결과적으로 any나 unkown을 사용하지 않고도 모든 타입의 값을 다 적용할 수 있는 범용적인 함수를 정의할 수 있다.

    제네릭 함수의 기본 타입 정의 방법은, 함수 이름 뒤에 꺽쇠(<>) 기호를 사용해 타입을 담는 변수인 타입 변수를 선언한다. 그리고 매개변수와 반환값의 타입에도 똑같이 타입 변수로 설정한다.

    아래 코드의 타입 변수 T는 함수가 호출될 때 타입이 결정된다. getNum(100) 처럼 number 타입을 인수(아규먼트)로 전달하면, 타입 변수 T는 number 타입으로 추론되어 매개변수(파라미터) value와 함수의 반환값 타입이 number 타입으로 결정된다.

    function getNum<T>(value: T): T { // (2) T === number
        return value;
    }
    
    let num = getNum(100); // (1) Type inference : number type
    console.log(num); // 100

    타입 변수의 타입 결정 (출처: 한 입 크기로 잘라먹는 타입스크립트)

     

    - 2개 이상의 타입 변수를 가진 제네릭 함수

    function swapFunc<T,U>(a: T, b: U) {
        return [b, a];
    }
    
    const [a, b] = swapFunc(1, true); // Type Inference: 'number', 'boolean'
    console.log(`a = ${a}, b = ${b}`); // a = true, b = 1

     

    - 다양한 타입의 원소를 가진 배열 타입을 인수로 받는 제네릭 함수

    function return1stEl<T>(data: T[]) {
        return data[0];
    }
    
    let num = return1stEl([0,1,2]); // T ==== number
    let str = return1stEl(["hello", "mynameis", 1]); // T ==== string | number
    console.log(`num = ${num}, str = ${str}`);

     

     

    2. Promise에서의 제네릭(Generic)


    자바스크립트에서 배운 것처럼, Promise 객체는 실행 함수(executor)가 인수 resolve와 reject를 콜백으로 받아서 처리 성공 여부에 따라 둘 중 하나를 반드시 호출한다. 이때 호출되는 콜백에 따라 내부 프로퍼티 PromiseStatePromiseResult가 결정된다. Promise 객체는 실행 함수의 결과나 에러를 .then, .catch, .finally 메서드를 통해 유연하게 활용할 수 있다.

      (1) 대기 - new Promise (executor): PromiseState ("pending"), PromiseResult(undefined)

      (2) 이행 - resolve(value) 호출: PromiseState ("fulfiiled"), PromiseResult (value)

      (3) 거부 - reject(error) 호출: PromiseState("reject"), PromiseResult (error)

    Promise 객체의 내부 프로퍼티 (출처: 모던 JavaScript, 프라미스)

     

    Promise 객체의 타입을 정의하는 방법은 new Promise 생성자 뒤에 꺽쇠(<>) 기호를 사용해 타입을 설정해주면 된다. 기본적으로 Promise 객체가 생성되면 unkown 타입이다. promise 객체의 인수로 들어오는 resolve와 reject 함수의 타입은 각각 아래와 같다.

    resolve 함수 (parameter) resolve: (value: unkown) => void

    reject 함수 (parameter) reject: (reason?: any) => void

    즉, void 타입의 resolve 함수는 실행 함수(executor)의 인수이며 기본적으로 unkown 타입의 결과값(인수)를 받는다. 마찬가지로 reject 함수도 void 타입이며 실행 함수의 인수로 들어가지만 결과값(인수)로 받는 타입은 resolve와 달리 any 타입의 선택 속성을 가진다.

    resolve와 reject 함수에 들어오는 인수의 타입은 unkown과 any 타입이므로 .then이나  .catch 메서드를 사용하는 경우 타입 좁히기(type narrowing)를 통해 안전하게 사용하는 것을 권장한다.

    unkown 속성을 가지는 resolve의 결과값
    any 속성을 가지는 reject의 결과값

    타입 좁히기는 보다 넓은 타입의 집합을 더 좁은 타입의 집합으로 재정의 하는 행위를 의미한다. 아래 코드의 then 구문에서 result는 if문을 통해 string | number 타입으로부터 string 단일 타입으로 타입을 좁혔다. 타입 좁히기를 통해 타입을 분명히 하고 에러를 줄일 수 있다.

    참고로 아래 코드 then 구문의 resolve의 결과값을 다루는 콜백함수에서는 콘솔 로그가 찍히지 않는다. 왜냐하면 대기 상태의 프라미스는 실행 함수(executor)를 통해 이행(resolved) 혹은 거부(rejected) 중 하나만 호출하여 처리한다. 처리가 끝난 뒤 resolve, reject를 호출하면 무시된다. 그렇기 때문에 setTimeout을 통해 1초 뒤에 동작하는 resolve 함수는 무시되고 reject만 호출되며 promise 객체의 처리는 끝난다.

    let promise = new Promise<number | string>((resolve, reject) => {
        setTimeout(() => resolve(100), 1000);
        reject("Error occurred!!");
    });
    
    promise.then(
        (result) => { // string | number type
            if (typeof result === "number") {
                console.log(result); // None. The executor has already finished execution with 'rejected'.
            }
        }
        ,(error) => { // any type
            if (typeof error === "string") { // Type narrowing
                console.log(error); // "Error occurred!!"
            }
        }
    );
    
    promise.catch((error) => { // any type
        if (typeof error === "string") { // Type narrowing
            console.log(error); // "Error occurred!!"
        }
    })

     

    - 인터페이스를 활용한 Promise 객체를 반환하는 함수의 타입 정의

    interface Info {
        name: string
        age: number
    }
    
    function fetchInfo() {
        return new Promise<Info>((resolve, reject) => {
            setTimeout(() => {
                resolve({
                    name: "IU",
                    age: 32
                });
            }, 1000);
        });
    }
    
    fetchInfo().then((response) => {
        // The type of response is Info
        console.log(response); // { "name": "IU", "age" : 32}
    })

     

    - 또는 더 직관적으로 아래와 같이 함수의 반환값 타입을 명시할 수도 있다.

    function fetchInfo(): Promise<Info> {
        return new Promise((resolve, reject) => { // Info type
            ...
        });
    }

     

    3. 제네릭의 제약조건


    제네릭은 Interface나 타입 별칭과 함께 사용할 수 있다. 아래 코드와 같이 반복되는 형태의 타입에 대해  새로운 타입 정의 없이 기존에 정의된 타입을 재사용하여 불필요한 코드를 최소화 할 수 있다.

    interface NewType<T> {
        id: number,
        contents: T
    }
    
    const sample1: NewType<string> = {
        id: 1,
        contents: 'TypeScript'
    };
    
    const sample2: NewType<number> = {
        id: 2,
        contents: 100
    };
    
    const sample3: NewType<boolean> = {
        id: 3,
        contents: false
    };
    
    const sample4: NewType<boolean[]> = {
        id: 4,
        contents: [true, false, true]
    };
    
    console.log(sample1, sample2, sample3, sample4);

     

    만약 타입 변수 T가 number과 boolean인 경우만 허용하는 경우, extends 키워드를 사용하는 제약 조건을 추가할 수 있다. 제약조건(Constraints)을 사용하는 방법은 꺽쇠 기호(<>) 내부에서 타입 변수 뒤에 extends 키워드와 함께 제한할 타입을 써주면 된다. 인터페이스의 확장과 달리 제약조건은 타입을 경우 수를 제한한다.

    interface NewType<T extends string | number> {
        id: number,
        contents: T
    }
    
    const sample1: NewType<string> = {
        id: 1,
        contents: 'TypeScript'
    };
    
    const sample2: NewType<number> = {
        id: 2,
        contents: 100
    };
    
    const sample3: NewType<boolean> = { // Error! type 'booean'
        id: 3,
        contents: false
    };
    
    const sample4: NewType<boolean[]> = { // Error! type 'boolean[]'
        id: 4,
        contents: [true, false, true]
    };

     

    - 타입 별칭 및 확장된 인터페이스에 제네릭의 제약 조건을 사용한 예시

    type U = string | number | boolean;
    
    // Constraints
    type NewType<T extends U> = undefined | T;
    
    const word: NewType<string> = "TypeScript";
    console.log(word); // "TypeScript"
    
    interface Animal {
        name: string;
        age: number;
    }
    
    // Constraints
    interface Dog<T extends U> extends Animal {
        breed: T;
    }
    
    const myDog: Dog<string> = {
        name: "Luna",
        age: 1,
        breed: "Maltese"
    };
    console.log(myDog);

     

    - extends 키워드는 위치에 따라 인터페이스의 확장, 제네릭의 제한조건 외에도 삼항 연산자를 사용하는 조건부 타입(Conditonal Types)에서도 사용된다.

     

     

     

    참고 사이트 1. 한 입 크기로 잘라먹는 타입스크립트

    참고 사이트 2. 한눈에 보는 타입스크립트

    참고 사이트 3. ts-for-jsdev

Designed by Tistory.