宮水の日記

宮水の日記

主に書評や資格取得について記事を書いています。

Typescript exercises やってみた 8問目

みなさんこんにちは。宮水です。
今日は、TypeScript エクササイズの8に取り組んでみました。

こちらのリポジトリをforkして、cloneして取り組みます。
rootディレクトリで、yarn installしてから問題文にあるRun this exerciseのコマンドを叩くと、答え合わせができます。
github.com

本日の問題

/*

Intro:

    PowerUsers idea was bad. Once those users got
    extended permissions, they started bullying others
    and we lost a lot of great users.
    As a response we spent all the remaining money
    on the marketing and got even more users.
    We need to start preparing to move everything to a
    real database. For now we just do some mocks.

    The server API format was decided to be the following:

    In case of success: { status: 'success', data: RESPONSE_DATA }
    In case of error: { status: 'error', error: ERROR_MESSAGE }

    The API engineer started creating types for this API and
    quickly figured out that the amount of types needs to be
    created is too big.

 PowerUsersのアイデアはダメでした。
 それらのユーザーが拡張された権限を取得すると、他のユーザーをいじめ始め、多くの優れたユーザーを失いました。
 対して、残りのすべてのお金をマーケティングに費やし、さらに多くのユーザーを獲得しました。
 すべてを実際のデータベースに移動する準備を始める必要があります。とりあえず、モックをいくつかやってみます。
 
    成功した場合: { status: 'success', data: RESPONSE_DATA }
    エラーの場合: { status: 'error', error: ERROR_MESSAGE }

  APIエンジニアは、このAPIのタイプの作成を開始し、作成する必要があるタイプの数が多すぎることをすぐに理解しました。

Exercise:

    Remove UsersApiResponse and AdminsApiResponse types
    and use generics in order to specify API response formats
    for each of the functions.

 UserのAPI応答とAdminのApiResponseタイプを削除し、ジェネリックを使用して、各関数のAPI応答形式を指定します。

Run:

    npm run 8

    - OR -

    yarn -s 8

*/

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' }
];

type AdminsApiResponse = (
    {
        status: 'success';
        data: Admin[];
    } |
    {
        status: 'error';
        error: string;
    }
);

function requestAdmins(callback: (response: AdminsApiResponse) => void) {
    callback({
        status: 'success',
        data: admins
    });
}

type UsersApiResponse = (
    {
        status: 'success';
        data: User[];
    } |
    {
        status: 'error';
        error: string;
    }
);

function requestUsers(callback: (response: UsersApiResponse) => void) {
    callback({
        status: 'success',
        data: users
    });
}

function requestCurrentServerTime(callback: (response: unknown) => void) {
    callback({
        status: 'success',
        data: Date.now()
    });
}

function requestCoffeeMachineQueueLength(callback: (response: unknown) => void) {
    callback({
        status: 'error',
        error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.'
    });
}

function logPerson(person: Person) {
    console.log(
        ` - ${chalk.green(person.name)}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
    );
}

function startTheApp(callback: (error: Error | null) => void) {
    requestAdmins((adminsResponse) => {
        console.log(chalk.yellow('Admins:'));
        if (adminsResponse.status === 'success') {
            adminsResponse.data.forEach(logPerson);
        } else {
            return callback(new Error(adminsResponse.error));
        }

        console.log();

        requestUsers((usersResponse) => {
            console.log(chalk.yellow('Users:'));
            if (usersResponse.status === 'success') {
                usersResponse.data.forEach(logPerson);
            } else {
                return callback(new Error(usersResponse.error));
            }

            console.log();

            requestCurrentServerTime((serverTimeResponse) => {
                console.log(chalk.yellow('Server time:'));
                if (serverTimeResponse.status === 'success') {
                    console.log(`   ${new Date(serverTimeResponse.data).toLocaleString()}`);
                } else {
                    return callback(new Error(serverTimeResponse.error));
                }

                console.log();

                requestCoffeeMachineQueueLength((coffeeMachineQueueLengthResponse) => {
                    console.log(chalk.yellow('Coffee machine queue length:'));
                    if (coffeeMachineQueueLengthResponse.status === 'success') {
                        console.log(`   ${coffeeMachineQueueLengthResponse.data}`);
                    } else {
                        return callback(new Error(coffeeMachineQueueLengthResponse.error));
                    }

                    callback(null);
                });
            });
        });
    });
}

startTheApp((e: Error | null) => {
    console.log();
    if (e) {
        console.log(`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`)
    } else {
        console.log('Success!');
    }
});

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/generics.html

答え

以下の5行を関数の上ら辺に追加しました。

type ApiResponse<T> = { status: 'success'; data: T; } | { status: 'error'; error: string; };

type ApiResponseOfAdmin = ApiResponse<Admin[]>;
type ApiResponseOfUser = ApiResponse<User[]>;

type CurrentServerTimeApi = ApiResponse<number>;
type CoffeeMachineQueueLengthApi = ApiResponse<number>;

感想

今回も問題を理解するのにめっちゃ時間かかったんですけど、とにかく`requestCurrentServerTime`関数と`requestCoffeeMachineQueueLength`関数
のunknown型をAdmin用でもUser用でもいいようにジェネリックを使って対応させてくださいという問題でした。

ジェネリックについては、以下のやめ太郎さんの記事が大変わかりやすいです。
4歳娘「パパ、具体的な名前をつけないで?」 - Qiita

今回は、Tの部分がAdminでもUserでもnumberでもOKな、ApiResponseという型を作りました。

type ApiResponse<T> = { status: 'success'; data: T; } | { status: 'error'; error: string; }

ApiResponseOfAdminとか命名イケテなさすぎますねwwww 英語頑張ります😭
ふぅー8問目もめちゃくちゃ難しかった😭 以上です!