Typescript Exercise 10 풀어보고 정리해보기
타입스크립트 학습을 위해 TypeScript exercises 문제를 풀어보고 기억 보관 목적으로 정리해보는 글입니다. 😀
➯ Exercise 10.
데이터를 요청하는 함수를 재실행하지 않기 위해,
기존의 코드 베이스로 Promise로 비동기 처리하는 함수를 생성해보기.
최종적으로 결과가 성공하면 데이터를, 실패하면 에러메시지를 리턴하는 Promise
객체를 리턴하도록 해야한다.
새로 만드는 함수의 이름은 promisify
로 명명한다.
+
모든 요청을 object로 담고 이 요청들을 각각 promisify를 하도록 하는 함수 promisifyAll
를 만들어보기.
아래 코드처럼 promisifyAll
가 리턴하는 객체를 api에 할당하기
const api = promisifyAll(oldApi);
타입 정의 및 사용 데이터
User
, Admin
각각의 인터페이스들은 유니온 타입(Union Type
)을 사용해 Person
이라는 타입을 생성한다.
type은 공통 필드지만 제한값이 다르고 occupation
은 User 인터페이스에, role
은 Admin 인터페이스에만 정의되어 있다.
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' }
];
const users: User[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' }
];
API 요청에 대한 응답결과를 정의하는 ApiResponse
를 타입으로 적용할때
함수처럼 적용될 수 있도록 제네릭T
을 사용한다. 여기에 User[]
, Admin[]
또는 그 외에 값 또한 사용이 가능하다.
export type ApiResponse<T> = (
{
status: 'success';
data: T;
} |
{
status: 'error';
error: string;
}
);
oldApi
에 각 요청이 정의되어 있다. 각 요청함수의 인자로 callback 함수가 사용되는데 이를 ApiFunction
타입으로
정의한다. 타입 정의할때, 제네릭을 사용해서 인자 타입인 ApiFunction
에서도 또한 사용될 수 있도록 한다.
type ApiFunction<T> = (callback: (response: ApiResponse<T>) => void) => void;
const oldApi = {
requestAdmins(callback: (response: ApiResponse<Admin[]>) => void) {
callback({
status: 'success',
data: admins
});
},
requestUsers(callback: (response: ApiResponse<User[]>) => void) {
callback({
status: 'success',
data: users
});
},
requestCurrentServerTime(callback: (response: ApiResponse<number>) => void) {
callback({
status: 'success',
data: Date.now()
});
},
requestCoffeeMachineQueueLength(callback: (response: ApiResponse<number>) => void) {
callback({
status: 'error',
error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.'
});
}
};
promisifyAll
함수는 각 요청함수를 담고있는 oldApi
를 인자로 받아서 요청들을 promisify
하는 역할을 한다.
export const api = promisifyAll(oldApi);
promisify
함수는 ApiFunction
타입의 콜백함수를 인자로 받고 () => Promise
타입으로 리턴하게 된다.
Promise객체 내부에서는 각 API 요청에 대한 결과를 처리하는 코드가 진행된다.
type ReturnPromise<T> = () => Promise<T>;
export function promisify<T>(arg: ApiFunction<T>): ReturnPromise<T> {
return () => new Promise<T>((resolve, reject) => {
arg((response) => {
if(response.status === 'success') {
resolve(response.data);
} else {
reject(new Error(response.error));
}
});
});
}
promisifyAll
함수는 각 요청의 콜백함수를 담은 object를 인자로 받고,
그 object를 리턴하되 value 값은 각 요청에 대해서 promisify 함수를 호출한 결과를 담아 리턴한다.
그 후에 object의 키값을 호출하면 API 응답결과를 사용할 수 있게 된다.
type ApiObject<T> = {[K in keyof T]: ApiFunction<T[K]>};
type PromiseResult<T> = {[K in keyof T]: ReturnPromise<T[K]>};
export function promisifyAll<T extends {[key: string]: any}>(obj: ApiObject<T>): PromiseResult<T> {
let result:Partial<PromiseResult<T>> = {};
for(const requestApi of Object.keys(obj)) {
result[requestApi as keyof ApiObject<T>] = promisify(obj[requestApi]);
}
return result as PromiseResult<T>;
}
PromisifyAll 함수 좀 더 알아보기
제네릭 타입으로 사용되는 T
의 형태를 아래와 같이 제한한다.
<T extends {[key: string]: any}>
처음에는 빈 객체가 기본값으로 할당되기 때문에 Partial 로 정의해주어야 오류가 나지 않는다.
let result:Partial<PromiseResult<T>> = {};
for .. of
에서 정의된 requestApi
은 string
타입이기 때문에 아래 result
객체의 키값으로 사용할 때
다시 한번 ApiObject<T>
의 키 타입으로 형변환 해주어야 한다.
for(const requestApi of Object.keys(obj)) {
result[requestApi as keyof ApiObject<T>] = promisify(obj[requestApi]);
}
ApiObject: `oldApi` 객체를 담는 타입
PromiseResult: ApiObject와 key 값은 같지만 `Promisify` 호출 결과인 `() => Promise` 를 담는 타입
promisifyAll
자체에서 T의 키인 K를 any
타입으로 정의했기 때문에 ApiFunction<any>
가 되고,
any
타입이기 때문에 promisify 내에서도 오류가 나지 않는 것 같다.
(promisify에서는 User[], Admin[], number 로 지정해서 사용되었다.)
이 부분이 제일 이해가 가지 않았는데 여기서 any
타입이 아닌 특정한 객체로 타입을 지정하게 되면
ApiFunction 사용할 때 타입 오류가 나기 때문일까?
any
타입 말고 다른 방법은 없을지 궁금하고 내가 추측한게 맞는지도 궁금하다. 🤔
type ApiObject<T> = {[K in keyof T]: ApiFunction<T[K]>};
type PromiseResult<T> = {[K in keyof T]: ReturnPromise<T[K]>};
아래 코드는 promisify된 요청들에 대한 응답을 사용하는 함수들이다.
function logPerson(person: Person) {
console.log(
` - ${person.name}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
);
}
async function startTheApp() {
console.log('Admins:');
(await api.requestAdmins()).forEach(logPerson);
console.log();
console.log('Users:');
(await api.requestUsers()).forEach(logPerson);
console.log();
console.log('Server time:');
console.log(` ${new Date(await api.requestCurrentServerTime()).toLocaleString()}`);
console.log();
console.log('Coffee machine queue length:');
console.log(` ${await api.requestCoffeeMachineQueueLength()}`);
}
startTheApp().then(
() => {
console.log('Success!');
},
(e: Error) => {
console.log(`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`);
}
);
모든 소스는 아래 링크에서 참고하였습니다
https://typescript-exercises.github.io/#exercise=10&file=%2Findex.ts