📘

7장 제네릭 타입

7.1 기본 타입 정의

7.1.1 제네릭이란?

타입스크립트는 제네릭이란 타입을 제공합니다. 이 제네릭 타입을 사용하면 다양한 타입에 대응할 수 있는 코드를 작성할 수 있습니다.

1) 제네릭을 사용하는 이유

function returnNumber(x: number): number { return x; } console.log(returnNumber(1)); // 1 console.log(returnNumber('1')); // Error function returnString(x: string): string { return x; } console.log(returnString('2')); // '2' console.log(returnString(2)); // Error
returnNumber 함수는 1개의 number 타입 매개변수 x를 받고 x를 그대로 반환합니다. returnString 함수는 1개의 string 타입 매개변수 x를 받고 x를 그대로 반환합니다.
두 함수는 각각 number와 string 타입에 대해서만 작동합니다. 이때 제네릭을 사용하면 2개의 함수로 나누지 않고 1개의 함수로 만들 수 있습니다.
function returnParam<T>(x: T): T { return x; } console.log(returnParam<number>(2)); console.log(returnParam<string>('2'));
<>안에 타입을 넘겨주지 않아도 전달되는 인수의 타입으로 T의 타입을 추론할 수 있습니다.
function returnParam<T>(x: T): T { return x; } console.log(returnParam(2)); console.log(returnParam('2'));

2) 제네릭 사용법

제네릭은 꺾쇠괄호(<>)와 타입 매개변수를 함께 사용해서 작성할 수 있습니다.
function pair<T, U>(x: T, y: U): [T, U] { return [x, y]; } console.log(pair<number, string>(1, 'a')); // [1, a]
pair 함수는 2개의 매개변수 x, y를 받아서 튜플로 반환하는 함수입니다. 꺾쇠괄호(<>) 안에 타입 변수인 T, U의 타입을 넣어 사용할 수 있습니다. 타입 매개변수는 마음대로 정할 수 있습니다. 일반적으로 대문자 알파벳 한 글자로 표현합니다.(T,U,K…)
 

7.2 콜백 함수에서의 제네릭

7.2.1 타입스크립트의 콜백 함수

다른 함수의 인자로 전달되어 호출되는 함수를 콜백 함수라고 합니다. 타입스크립트에서는 매개변수와 반환 타입을 명시하여 보다 높은 안정성을 제공합니다.

1) 콜백 함수 타입 정의

type Callback = (data: string) => void;
Callback : 타입의 이름입니다.
(data: string) => void : 함수 타입 표현식으로, string 타입의 매개변수(data)를 받는 함수를 뜻합니다.
이처럼 type을 사용하여 함수 타입과 매개변수 타입을 명시합니다.

2) 콜백 함수 사용 예제

// 콜백 함수 타입 정의 type Callback = (result: string) => void; // 콜백 함수 정의 const funcCallback = (callback: Callback) => { const result = "callback"; callback(result); }; // 콜백 함수 사용 funcCallback((data) => { console.log("콜백 함수 사용 중", data); // 콜백 함수 사용 중 callback });
type을 통해 콜백 함수 타입을 정의합니다. 콜백 함수를 정의하면서 매개변수를 Callback 타입으로 지정합니다. data라는 string 매개변수를 전달받고, 그 값에 result 값을 할당합니다.

7.2.2 콜백 함수에 제네릭 적용

1) 제네릭 타입 정의

type Callback<T> = (result: T) => void;
Callback 이란 제네릭 타입을 정의합니다. T라는 매개변수를 받는 함수를 나타내며 반환 값은 없습니다.

2) 제네릭 적용

// 제네릭을 적용한 콜백 함수 타입 정의 type GenericCallback<T> = (result: T) => void; // 제네릭을 적용한 콜백 함수 정의 const funcGenericCallback = <T>(callback: GenericCallback<T>, data: T) => { callback(data); }; // 제네릭 콜백 함수 호출 funcGenericCallback<string>((data) => { console.log("콜백 함수 사용", data); }, "제네릭 사용 중");

7.2.3 다양한 타입의 제네릭 콜백 함수

제네릭을 활용하면 콜백 함수를 더 다양하고 유연하게 사용할 수 있습니다. 이를 통해 함수의 매개변수나 반환 값의 타입을 미리 지정하지 않고, 실행 시점에 동적으로 결정할 수 있습니다.
두 가지 타입 이상을 사용하는 제네릭 콜백 함수를 아래 예시를 통해 살펴보겠습니다.
// 제네릭 콜백 함수 타입 정의 type MultiTypeCallback<T, U> = (param1: T, param2: U) => void; // 제네릭 콜백 함수 정의 const funcMultiCallback = <T, U><callback: MultiTypeCallback<T, U>( param1: T, param2: U ) => { callback(param1, param2); };
MultiTypeCallback 에서 제네릭 타입인 T, U 두 가지의 타입을 정의합니다. 두 가지 타입을 따르는 param1param2 를 매개변수로 받는 반환 값이 없는 형태입니다.

1) <string, number> 사용 예제

funcMultiCallback<string, number>((fruits, count) => { console.log("fruits: ", fruits); console.log("count: ", count); }, "apple", 30) // fruits: apple // count: 30
제네릭 타입을 <string, number>로 지정하여 매개변수를 전달합니다. 콜백 함수 내에서 받은 매개변수 fruitscount를 출력합니다.

2) <number[], function> 사용 예제

funcMultiCallback<number[], (value: number) => void>((result, func) => { result.forEach((num) => func(num * 2)); }, [1, 2, 3], (value) => console.log("Double: ", value)); // Double: 2 // Double: 4 // Double: 6
제네릭 타입을 number[], (value: number) => void)로 지정하여 매개변수를 전달합니다. 콜백 함수 내에서 받은 매개변수 number[] 의 요소를 전달받은 함수를 통해 두 배로 출력합니다.

7.2.4 제네릭 콜백 함수를 통한 API 호출

데이터의 타입을 제네릭으로 지정하여 API를 호출할 수 있습니다. 이를 통해 안정성을 유지하면서도 함수의 재사용성을 향상시킬 수 있습니다.
// 타입 정의 type Success<T> = (data: T) => void; type Error = (error: string) => void; function fetchData<T>(url: string, successCallback: Success<T>, errorCallback: Error) { const res = { success: true, data: "API Data" }; if (res.success) { successCallback(res.data); } else { errorCallback("API 에러"); } } // 사용 예제 fetchData<string>( "/api/data", (result) => { console.log("API 호출 성공:", result); }, (error) => { console.error("API 호출 실패:", error); } );
SuccessCallbackErrorCallback 두 가지 콜백 함수 타입을 정의합니다. fetchData 함수에서 제네릭 타입 T를 받아 호출 결과를 콜백 함수로 전달합니다. 호출 시 제네릭 타입을 명시하여 성공 시와 실패 시 실행될 콜백 함수를 전달합니다.
 

7.3 인터페이스와 타입 별칭에서의 제네릭

7.3.1 제네릭 인터페이스

1) 제네릭 인터페이스

제네릭 인터페이스는 제네릭 타입을 활용하여 재사용 가능한 타입을 가진 인터페이스를 정의하는 것입니다. 제네릭 인터페이스는 특정 타입을 파라미터로 받아 여러 종류의 타입을 적용할 수 있는 기능을 제공합니다.
이를 통해 동일한 인터페이스를 다양한 타입에 대해 사용할 수 있고 코드의 재사용성과 안정성을 향상시킬 수 있습니다.
interface Person<K, V> { name: K; age: V; }
위 코드는 Person이라는 제네릭 인터페이스를 정의한 예시입니다. 위 인터페이스는 nameage라는 2개의 프로퍼티를 가지며 nameK 타입의 값을 ageV 타입의 값을 가집니다. 해당 타입들은 실제 사용할 때 지정한 구체적인 타입으로 대체됩니다.
const person: Person<string, number> = { name: "John", age: 28, };
Person<string, number> 타입으로 선언된 person 객체는 string 타입인 name , number 타입인 age를 가지도록 정의됩니다. 이렇게 각 프로퍼티의 타입을 타입 변수에 직접 할당하여 정의하고 사용해야 합니다.

2) 제네릭 인터페이스와 인덱스 시그니처

제네릭 인터페이스는 객체의 인덱스 시그니처 문법과 결합하여 더욱 유연한 객체를 만들 수 있습니다.
인덱스 시그니처는 객체의 프로퍼티에 동적인 키를 사용할 수 있도록 해주며 제네릭 인터페이스와 인덱스 시그니처를 함께 사용하면 객체의 프로퍼티를 동적으로 확장할 수 있다는 장점이 있습니다.
interface Person<K, V> { name: K; age: V; [key: string]: K | V; } const person: Person<string, number> = { name: "John", age: 28, address: "Main Street 1", gender: "male", };
위 코드에서 Person 인터페이스는 nameage라는 고정된 프로퍼티를 가지고 있습니다. 그리고 [key: string]: K | V라는 인덱스 시그니처를 추가하여 객체에 동적인 프로퍼티를 추가할 수 있도록 정의되었습니다. key는 동적인 프로퍼티의 키를 나타내고 K | V는 타입 변수로 할당된 string, number 타입 중 1가지로 정의되는 것을 의미합니다. person 객체는 name , age 프로퍼티 외에도 address , gender라는 프로퍼티를 가지고 있습니다. 위 2가지의 프로퍼티는 인덱스 시그니처로 동적인 프로퍼티로 추가함으로써 객체의 유연성을 높일 수 있습니다.

3) 제네릭 인터페이스 확장

제네릭 인터페이스를 활용하여 단일타입을 인터페이스로 확장할 수 있습니다. 이를 통해 타입의 구조를 재사용하고 기존 인터페이스를 확장하면서 유연한 타입을 정의할 수 있습니다.
interface Person<T> { name: T; age: number; } interface Developer extends Person<string> { job: string; } const myinfo: Developer = { name: "John", age: 28, job: "Frontend Developer", };

7.3.2 제네릭 타입 별칭

1) 제네릭 타입 별칭

제네릭 타입 별칭은 제네릭 타입을 사용하여 새로운 타입을 정의하는 방법입니다. 제네릭 타입 별칭의 사용 방법은 제네릭 인터페이스와 유사하며 마찬가지로 코드의 재사용성과 안정성을 향상시킬 수 있습니다.
type Person<K, V> = { name: K; age: V; } const person: Person<string, number> = { name: "Emily", age: 22, };
위 코드에서 Person<string, number> 타입으로 선언된 person은 string 타입인 name , number 타입인 age를 가지도록 정의됩니다. 제네릭 인터페이스와 동일하게 각 프로퍼티의 타입을 타입 변수에 직접 할당하여 정의하고 사용해야 합니다.

2) 제네릭 타입 별칭과 인덱스 시그니처

제네릭 타입 별칭은 제네릭 인터페이스와 마찬가지로 인덱스 시그니처 문법을 사용함으로써 더욱 유연한 객체를 만들 수 있습니다.
type Person<K, V> = { name: K; age: V; [key: string]: K | V; } const person: Person<string, number> = { name: "Emily", age: 22, address: "Main Street 2", gender: "female", };

7.3.3 제네릭 인터페이스 활용

interface Seller { type: "seller"; salesList: string[]; } interface Consumer { type: "consumer"; orderList: string[]; } interface User<T> { name: string; profile: T; } function selesProduct(user: User<Seller>) { console.log(user.profile.salesList); }
User 인터페이스는 nameprofile 프로퍼티를 가지고 있습니다. profile 프로퍼티는 타입 변수인 <T> 타입을 가집니다. selesProduct 함수는 User<Seller> 타입의 인자를 받습니다. 즉, User 인터페이스의 profile 프로퍼티는 Seller 인터페이스를 따르는 객체여야 합니다.
 
let seller: User<Seller> = { name: "John", profile: { type: "seller", salesList: ["Laptop", "Cell Phone"], }, }; let consumer: User<Consumer> = { name: "Emily", profile: { type: "consumer", orderList: ["Laptop"], }, }; selesProduct(consumer); // Error selesProduct(seller); // ["Laptop", "Cell Phone"]
selesProduct(consumer) 호출 시에는 User<Seller> 타입의 인자를 요구하므로 타입 불일치로 인해 오류가 발생합니다. 하지만 selesProduct(seller)를 호출하면 ["Laptop", "Cell Phone"] 이 출력됩니다.
이렇게 제네릭 인터페이스를 활용하면 원하는 타입을 쉽게 지정할 수 있으며, 해당 타입을 사용하는 함수나 변수에 제한을 두어 타입 좁히기를 사용하지 않고도 타입 안정성을 높일 수 있습니다.
 

7.4 클래스에서의 제네릭

제네릭은 클래스 타입에도 적용되어 다양한 데이터 타입에 대해 재사용 가능한 코드를 작성할 수 있게 해줍니다. 제네릭 클래스를 사용하면 클래스의 프로퍼티와 메서드가 여러 데이터 타입에서 작동할 수 있습니다.

7.4.1 원시 타입 변수

간단한 예시를 통해 기본적인 제네릭 클래스의 구조를 살펴보겠습니다.
class Person<T, K> { private _name: T; private _age: K; constructor(name: T, age: K) { this._name = name; this._age = age; } }
위 코드에서는 타입 변수로 T, K를 지정하여 Person 클래스를 선언함을 알 수 있습니다. 이러한 제네릭 타입 매개변수를 통해 클래스의 인스턴스를 생성할 때 특정 타입을 지정할 수 있습니다. 제네릭을 선언함으로써 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하여 타입 안정성을 높이면서도, Person 클래스를 다양한 타입에 대해 유연하게 사용할 수 있습니다. 예를 들어, string 타입으로 이름과 number로 나이를 지정할 수도 있고, 다른 타입을 사용할 수도 있습니다.
new Person('Jake', 12); // 인자를 통해 타입 추론 new Person<string, number>('Jake', 12); // 명시한 타입과 인자 타입 일치 new Person<string, number>(12, 'Jake'); // Error: 명시한 타입과 인자 타입 불일치
생성자로 인스턴스를 생성할 시에는 ‘타입 변수를 명시적으로 지정하는 경우’와 ‘인자를 통해 타입을 추론하는 경우’가 있습니다. 해당 변수의 타입을 명시적으로 지정하지 않을 경우에는 타입스크립트 컴파일러가 해당 변수의 타입을 자동으로 추론하며, 타입 변수의 타입을 지정해 주는 경우에는 인자와 타입이 일치해야 하고 그렇지 않으면 에러를 반환합니다.

7.4.2 참조 타입 변수

클래스에서의 제네릭은 원시 타입(string, number, boolean 등)뿐만 아니라 배열, 객체, 함수 등과 같은 모든 유형의 타입을 다룰 수 있습니다. 즉 제네릭을 사용하여 클래스를 정의할 때, 클래스가 다룰 데이터의 타입을 동적으로 설정할 수 있습니다.
예를 들어, 배열을 다루는 제네릭 클래스를 구현해 보겠습니다.
class Stack<T> { private _items: T[] = []; push(item: T): void { this._items.push(item); } pop(): T | undefined { return this._items.pop(); } }
이제 Stack 클래스를 생성할 때 어떤 타입의 요소를 저장할 것인지 명시할 수 있습니다.
const numberStack = new Stack<number>(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop()); // 2 const stringStack = new Stack<string>(); stringStack.push("hello"); stringStack.push("world"); console.log(stringStack.pop()); // "world"
만약 타입 변수 T에 number를 할당하여 Stack의 인스턴스를 생성한다면, 해당 스택은 오직 number 타입의 요소만을 저장할 수 있으며 다른 모든 타입은 허용되지 않습니다. 마찬가지로 타입 변수를 string으로 지정하여 스택을 생성하면, 해당 스택은 string 타입의 요소만을 저장할 수 있습니다.
따라서, 제네릭 클래스인 Stack을 사용함으로써 특정 타입의 요소만을 스택에 저장하고, 다른 타입의 요소는 거부할 수 있게 됩니다.

7.4.3 제네릭 클래스의 extends

클래스 선언 시 타입 변수의 extends 키워드는 제네릭 타입 매개 변수에 대한 제한을 설정할 때 사용되는데, 일반적인 클래스의 extends와는 조금 다릅니다. 일반적으로 클래스에서 extends 키워드는 하위 클래스가 상위 클래스를 확장하는 것을 의미합니다. 그러나 제네릭에서의 extends는 이런 상속 개념과는 대조됩니다.
제네릭에서의 extends는 특정한 타입 또는 그 타입의 서브타입(subtype)을 나타내는 것이 아니라, 해당 타입을 확장(extend)할 수 있는 범위를 지정하는 것을 의미합니다. 즉, 제네릭 타입 매개변수가 특정한 타입 또는 그 타입들의 서브타입으로 제한될 수 있도록 허용하는 것입니다.
이는 제네릭 타입 매개변수가 특정한 타입을 포함하여 이를 확장할 수 있다는 것을 의미합니다. 그러므로 extends 키워드를 사용하여 특정 타입을 제한함으로써, 해당 제네릭 클래스나 함수는 특정 타입 또는 그 타입의 서브타입만을 다룰 수 있게 됩니다.
설명만으로는 이해가 어려우니 간단한 예시를 통해 접근하겠습니다.
class Pocket<T extends number | string> { private _item: T; constructor(item: T) { this._item = item; } getItem(): T { return this._item; } }
위의 코드에서 Pocket 클래스는 제네릭 타입 매개변수 T를 사용하며, 이 매개 변수는 extends 키워드를 사용하여 number 또는 string 타입만을 허용하도록 제한되어 있습니다. 여기서 extends 키워드는 제네릭 타입 매개변수 T가 number 또는 string 타입을 포함하여 이를 확장할 수 있다는 것을 의미합니다. 즉, T는 number 또는 string 타입일 수도 있고, 이들의 서브타입 중 하나일 수도 있습니다.
const pocket1 = new Pocket<number>(10); // OK const pocket2 = new Pocket<string>("hello"); // OK const pocket3 = new Pocket<boolean>(true); // Error: 'boolean' 형식의 인수는 'number | string' 형식의 매개 변수에 할당될 수 없습니다.
이러한 제한을 통해 Pocket 클래스는 오직 number 또는 string 타입의 값만을 다룰 수 있게 됩니다.
 

7.5 프로미스에서의 제네릭

비동기 작업에 필수적인 프로미스 객체에서 타입스크립트의 적용과 제네릭을 어떻게 사용하는지 살펴봅니다.

7.5.1 프로미스

자바스크립트에서 배운 것처럼, 프로미스 객체는 실행 함수(Executor)가 인수 resolve와 reject를 콜백으로 받아서 처리 성공 여부에 따라 둘 중 하나를 반드시 호출합니다. 이때 호출되는 콜백에 따라 내부 프로퍼티 PromiseState와 PromiseResult가 결정됩니다. 프로미스 객체는 실행 함수의 결과나 에러를 .then, .catch, .finally 메서드를 통해 유연하게 활용할 수 있습니다.
  • 대기 - new Promise (executor): PromiseState ("pending"), PromiseResult(undefined)
  • 이행 - resolve(value) 호출: PromiseState ("fulfiiled"), PromiseResult (value)
  • 거부 - reject(error) 호출: PromiseState("reject"), PromiseResult (error)
프로미스 객체의 내부 프로퍼티프로미스 객체의 내부 프로퍼티
프로미스 객체의 내부 프로퍼티

7.5.2 프로미스의 타입

프로미스에는 인스턴스 객체, 실행 함수, 콜백 함수, 콜백 함수의 인자 모두 타입이 존재하여 헷갈릴 수 있는 여지가 큽니다. 타입을 지정하는 방법을 배우기 전에 하나하나 어떤 타입과 특징을 가지는지 살펴보겠습니다.
실행 함수로 초기화된 프로미스 객체의 인스턴스, 즉 새로 생성된 프로미스 객체는 기본적으로 Promise<unknown> 타입입니다. 여기에 사용자가 타입을 지정해 주면 인스턴스의 타입과 resolve 함수의 인수(결괏값 또는 value) 타입이 해당 타입으로 결정됩니다.
실행 함수는 void 타입을 가지고 있습니다. 그리고 실행 함수의 인수이자 콜백인 resolvereject 함수의 타입 또한 void이며 타입 어노테이션 방식으로 변경할 수 없습니다. 콜백 함수 인수의 타입 또한 직접적으로 변경할 수 없습니다. resolve 함수의 인수는 앞서 설명한 내용과 같이 프로미스 객체의 타입에 따라 결정되며, reject 함수의 인수는 변경되지 않으며 any 선택적 프로퍼티를 가집니다.
  • resolve 함수 - (parameter) resolve: (value: unknown) => void
    • unkown 프로퍼티을 가지는 resolve의 결괏값unkown 프로퍼티을 가지는 resolve의 결괏값
      unkown 프로퍼티을 가지는 resolve의 결괏값
  • reject 함수 - (parameter) reject: (reason?: any) => void
    • any 프로퍼티를 가지는 reject의 에러 값any 프로퍼티를 가지는 reject의 에러 값
      any 프로퍼티를 가지는 reject의 에러 값
즉, void 타입의 resolve 함수는 실행 함수(executor)의 인수이며 기본적으로 unknown 타입의 인수(결괏값 또는 value)를 받습니다. 마찬가지로 reject 함수도 void 타입이며 실행 함수의 인수로 들어가지만 인수(에러 값 또는 reason)의 타입은 resolve와 달리 any 타입 또는 값이 없는 any의 선택적 프로퍼티를 가집니다.

7.5.3 프로미스 타입의 추론

타입 지정 없이 new Pormise 키워드를 통해 생성된 프로미스 객체는 기본적으로 Promise<unknown> 타입으로 추론됩니다. resolve 함수에 값을 넣어 즉시 이행(fulfilled) 상태로 만들어도 타입은 같습니다. 그렇기 때문에 새로운 프로미스 객체를 생성할 때는 명시적으로 타이핑을 해줌으로써 실행 함수의 콜백에 들어가는 인수 타입을 특정 타입으로 제한할 수 있습니다.
그런데 new Promise 키워드를 사용하지 않고, Promise 키워드와 정적 메서드(Static Methods)를 사용해 새로운 프로미스 객체를 생성할 수도 있습니다. 이 경우 프로미스 객체는 Promise<unknown> 타입이 아닌 사용하는 메서드와 매개변수에 따라 다른 타입을 가지게 됩니다. 대표적인 프로미스의 정적 메서드인 .resolve, .reject 메서드를 봅시다.
프로미스 객체는 .resolve 메서드를 사용할 경우에 인수(결괏값 또는 value)의 타입에 따라 프로미스 객체의 타입이 추론(변경) 됩니다. 인수로 아무것도 넣지 않으면 Promise<void> 타입을 가지고, 인수를 넣어주면 인수의 타입에 따라 프로미스 객체의 타입이 결정됩니다. 하지만 .reject 메서드의 경우는 인수의 타입에 관계없이 Promise<never> 타입으로 추론됩니다.
const emptyPromise = new Promise(() => {}); // Promise<unknown> const basicPromise = new Promise((resolve) => resolve(200)); // Promise<unknown> const resolvedPromise = Promise.resolve(); // Promise<void> const resolvedNumberPromiseResolve = Promise.resolve(100); // Promise<number> const rejectedPromise = Promise.reject('Error'); // Promise<never>

7.5.4 프로미스 객체의 타입 정의

프로미스 객체의 인스턴스 타입을 정의하는 방법에는 두 가지가 있습니다. 간단히 new Promise 생성자 뒤에 꺾쇠괄호(<>) 기호를 사용해 타입을 지정하거나, 타입 어노테이션 방식으로 인스턴스 이름 옆에 :Promise<T> 형태로 타입을 명시할 수 있습니다.
// 타입 정의 방법 (1) const numPromise = new Promise<number>(() => {}); // Promise<number> // 타입 정의 방법 (2) const stringPromise:Promise<string> = new Promise(() => {}); // Promise<string> const simplePromise = new Promise<number>((resolve, reject) => { // promise<number> resolve(200); // (parameter) resolve: (value: number | PromiseLike<number>) => void reject('Error 404'); // (parameter) reject: (resean?: any) => void }); simplePromise.then((result) => console.log(result)); // 200
resolvereject 함수에 들어오는 인수의 타입은 unknown과 any 타입이므로 .then이나 .catch 메서드를 사용하는 경우 타입 좁히기(Type Narrowing)를 통해 안전하게 사용하는 것을 권장합니다. 여기서 타입 좁히기란 보다 넓은 타입의 집합을 더 좁은 타입의 집합으로 재정의하는 행위를 의미합니다.
아래 코드의 then 구문에서 result는 if문을 통해 string | number 타입으로부터 string 단일 타입으로 타입을 좁혔습니다. 타입 좁히기를 하면 타입을 분명히 하고 에러를 줄일 수 있습니다.
let typedPromise = new Promise<number | string>((resolve, reject) => { setTimeout(() => resolve(100), 1000); reject("Error occurred!!"); }); typedPromise.then( (result) => { // string | number type if (typeof result === "number") { console.log(result); // resolve 함수 동작은 무시됩니다. } } ,(error) => { // any type if (typeof error === "string") { // Type Narrowing console.log(error); // Error } } ); typedPromise.catch((error) => { // Error : any type if (typeof error === "string") { // Type Narrowing console.log(error); // Error } });
참고로 아래 코드 then 구문의 resolve의 결괏값을 다루는 콜백 함수에서는 콘솔 로그가 찍히지 않습니다. 왜냐하면 대기 상태의 프로미스는 실행 함수(Executor)를 통해 이행(resolved) 혹은 거부(rejected) 중 하나만 호출하여 처리합니다. 처리가 끝난 뒤 resolve, reject를 호출하면 무시됩니다. 그렇기 때문에 setTimeout을 통해 1초 뒤에 동작하는 resolve 함수는 무시되고 reject만 호출되며 promise 객체의 처리는 끝납니다.

7.5.5 프로미스 객체를 반환하는 함수의 타입 정의

인터페이스를 활용해 제네릭 형태로 함수의 반환값(프로미스)의 타입을 정의했습니다.
interface Info { name: string; age: number; } function fetchInfo() { return new Promise<Info>((resolve, reject) => { setTimeout(() => { resolve({ name: "Kim", age: 32, }); }, 1000); }); } fetchInfo().then((response) => { // The type of response is Info console.log(response); // { "name": "Kim", "age" : 32} });
 
아래와 같이 더 직관적으로 함수의 반환값 타입을 명시할 수도 있습니다.
function fetchInfo(): Promise<Info> { return new Promise((resolve, reject) => { // Info type ... }); }
 

7.6 제약 조건 (Constraints)

7.6.1 제네릭의 제약 조건

제네릭은 아래 코드와 같이 반복되는 형태의 타입에 대해 새로운 타입 정의 없이 기존에 정의된 타입을 재사용하여 불필요한 코드를 최소화할 수 있습니다.
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);
하지만 모든 타입을 포괄할 수 있는 제네릭은 마치 any를 연상케 합니다. 타입은 엄격하게 관리될수록 코드에서 에러가 발생할 확률이 줄어듭니다. 이 때문에 제네릭에는 타입 좁히기와 같이 타입을 제한할 수 있는 문법이 존재합니다. 만약 타입 변수 T가 string과 number인 경우만 허용하는 경우, 제네릭에 extends 키워드를 사용하여 제약 조건(Constraints)을 추가할 수 있습니다.
제약 조건을 사용하는 방법은 꺾쇠괄호(<>) 기호 내부에서 타입 변수 뒤에 extends 키워드와 함께 제한할 타입을 써주면 됩니다. 인터페이스의 확장과 달리 제약 조건은 타입을 경우 수를 제한합니다. 아래 코드에서는 타입 변수 T를 string 또는 number 타입으로 제약 조건을 주었습니다.
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], };

7.6.2 제약 조건의 다른 예제

1) 타입 별칭과 함께 사용한 제네릭의 제약 조건

type U = string | number | boolean; // Constraints type MyType<T extends U> = undefined | T; const word: MyType<string> = "TypeScript"; console.log(word); // "TypeScript"

2) 확장된 인터페이스와 함께 사용한 제네릭의 제약 조건

type U = string | number | boolean; 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)에서도 사용됩니다.