試行錯誤

仕事や趣味(スマブラ)など

りあクト!4-6 Result<T, E> の実装 コード解説

本ポストは「りあクト!TypeScriptで始めるつらくないReact開発 第3.1版」の「第4章 4-6. 型アサーションと型ガード」の末尾に掲載されたコードの解説になります。

背景

このコードの解読が教本に載っている内容の総復習になるとのことで、しっかりと理解できているかを確かめるために自分なりに記事にしようと思いました。

解説

基本的にコードの中にコメントを追加する形式で書いていきます。

//getUser: (userId: string) => Promise<User>

// 1. まず実行できるようにgetUserを簡素ながら自分で実装してみます。
// APIとして動いているという体です。
// 1-1 Type Userの定義
type User = {
    name: string;
};
// 1-2 getUserの定義
// 1行目の定義から、返り値の型はPromise<User>にする
// Promise<User> = Userを返すPromise 
const getUser = (userId:string):Promise<User> => {
    return new Promise((resolve, reject)=>{
        try{
            resolve(success());
        }catch{
            reject(failure());
        }
    });
};
// 成功した場合の処理
const success = (): User =>{
    return {name: 'User1'}
};
// 失敗した場合の処理
const failure = ():Error =>{
    return new Error('not found');
};


// 2. 教科書の実装コードの内容
// type Resultの方はOkもしくはErr。
// そしてそれぞれがTとEのジェネリクスパラメータを持つ
// OkまたはErrであるという2つの分岐処理を実装するため
// Result, Ok, ErrそれぞれにはT, E両方の型が必要
// (消化しきれていないポイント)
type Result<T, E extends Error> = Ok<T,E> | Err<T,E>;

class Ok<T, E extends Error>{
    // getUserを指定した場合TはUserになるので、
    // data.valの型はUserになる
    constructor(readonly val: T){}
    isOk = ():this is Ok<T,E> => true;
    isErr = (): this is Err<T,E> => false;
}

class Err<T, E extends Error>{
    constructor(readonly err: E){}
    isOk = ():this is Ok<T,E> => false;
    isErr = (): this is Err<T,E> => true;
}

const withResult = <T, A extends any[], E extends Error>(
    // "Aを引数としてTを返り値とするPromiseを返す関数"
    //  がwithResultの引数には入る
    
    // Tはfnの返り値の型になる。
    // なので、getUserを指定した場合はTはUserとなる。
    fn: (...args: A) => Promise<T>
    ) => async (...args: A): Promise<Result<T,E>> => {
        try{
            // 関数fnの結果をawaitで待って、
            // その返り値を使ってクラスOkをインスタンス化。
            return new Ok(await fn(...args));
        }catch(error){
            // キャッチしたerrorがErrorクラスのインスタンスの場合は、
            // errorを使ってErrorをインスタンス化
            if(error instanceof Error){
                return new Err(error as E);
            }
            // as Eにしてるのでコンパイルエラーは避けられる
    //(型アサーション)

            // return new Err(error as E);
            // TypescriptPlaygroundでエラーになるため上の一行が必要
            // 無いとPromise<Result<T,E>>がエラーになる
            // これは、error instanceof Errorがfalseの場合、
            //返り値がvoidとなり宣言に合わないからである。
        }
};

// 3. ちょっとした実験パート
// withResult(getUser)('patty') この形に疑問を抱いたので
// 試してみると結果は以下のようになる(★に繋がる)
const fnFn = () => async() => {
    console.log("Hello World");
}
console.log(fnFn());    // async() =>{ console.log("Hello World");}
console.log(fnFn()());  // "Hello World"


// 4. 実行パート 
// awaitを使うため全体をasyncの関数として括りまとめて実行する。
const main = async() =>{
    // withResult(getUser)でasyncの関数を返している(★)
    // その関数の引数として('patty')が使われている。
    const data = await withResult(getUser)('patty');
    if(data.isErr()){
        // 型ガードを使用している
        // data.errはwithResultの結果がErrの時のみ持っているプロパティ
        console.error(data.err);
    }else{
        // data.isErr()がfalseの時
        // data.valにgetUser('patty)の結果が格納されている。
        // data.valはResultの結果がOkの時のみ持っているプロパティ。
        // dataにはwithResultの時点でgetUserを引数に指定しているため、
        // data.valの型はUserになる。
        // なのでuser.nameはコンパイルエラーにならない
        const user = data.val;
        console.log(`Hello, ${user.name}!`);
    }    
}

main();

個人的に消化しきれていないポイント

type Result<T, E extends Error> = Ok<T,E> | Err<T,E>;

このOkとErrそれぞれが、TとEのジェネリクスパラメータを持たなければならない理由が理解しきれていない。
OkとErrという実装方針では作れないのか。
この点については理解できたらまた追記いたします。

おわりに

以上となります。
今回の解説について、間違っている点などありましたら是非コメント等で教えてください!