ステート管理ライブラリのalmin@0.17.0をリリースしました。

詳しい変更点は次の記事でも紹介しています。

⭐ Feature

Support context.useCase#execute typing #342 #107

AlminはTypeScriptを公式にサポートしています。

しかし今まではcontext.useCase(someUseCase).execute(args)argsany[]となっていました。 コレを解消するためにAlmin 0.13context.useCase(someUseCase).executor(useCase => useCase.execute(args))という型がチェックされるためだけのメソッドを追加していました。

しかし、Almin 0.17ではcontext.useCase(someUseCase).execute(args)も型チェックがほとんどのケースで効くようになりました。

Almin 0.17+では次のケースで型チェックが効くようになっています。 この例ではMyUseCaseA.prototype.executestring型ではない値を渡すとコンパイルエラーです。

import { UseCase, Context } from "almin";
class MyUseCaseA extends UseCase {
    execute(text: string) {}
}
const context = new Context({
    store: createStore({ name: "test" })
});

// valid
context.useCase(new MyUseCaseA()).execute("A");
// invalid
context.useCase(new MyUseCaseA()).execute(); // no argument
context.useCase(new MyUseCaseA()).execute(1); // can not pass number
context.useCase(new MyUseCaseA()).execute(1, 2); // can not pass number

Almin 0.17のexecute()の型チェックはまだ不完全な部分があり、次のように定義された仮引数よりも多い引数を渡してもエラーにはなりません。

import { UseCase, Context } from "almin";
class MyUseCaseA extends UseCase {
    execute(text: string) {}
}
const context = new Context({
    store: createStore({ name: "test" })
});
// **valid in almin 0.17**
context.useCase(new MyUseCaseA()).execute("A". 42);

Conditional Typesを使った実装

このexecute()の型チェックはTypeScript 2.8で追加されたConditional typesを使って実装しています。

まず、関数の引数の値の型をTupleとして返すtype functionを実装します。

type A0<T> = T extends () => any
    ? any : never
type A1<T> = T extends (a1: infer R1) => any
    ? [R1] : [never]
type A2<T> = T extends (a1: infer R1, a2: infer R2) => any
    ? [R1, R2] : [never, never]
type A3<T> = T extends (a1: infer R1, a2: infer R2, a3: infer R3) => any
    ? [R1, R2, R3] : [never, never, never]

コレを使えば次のようにfn関数の1番目の引数の型を含むTupleを取得できます。 fn関数の一番の引数はstring型なので[string]が手に入ります。

type A1<T> = T extends (a1: infer R1) => any
    ? [R1] : [never]

function fn(a1: string){

}

type FnArgs = A1<typeof fn>;
// [string]

context.useCase(someUseCase).execute(args)someUseCase#executeの引数の型を取得するのが目的です。 AlminではUseCaseExecutor(context.useCaseはこれを作って返す)というUseCaseの実行ラッパーを経由してUseCaseを実行します(ログなどを取るため)

そのため、UseCaseExecutor#excute(args)argssomeUseCase#execute(args)argsと同じ型であればよいはずです。

さきほど定義したA0...A3(実際はA9まであります)を使って、ひたすら型マッピングする定義をexecuteに追加しています。

class UseCaseExecutor<T extends UseCase> {
    useCase: T;

    constructor(useCase: T) {
        this.useCase = useCase;
    }

    // `this: never` aim to throw error when no arguments with argumented required `execute`
    execute<P extends A0<T["execute"]>, K>(this: P extends never ? never : this): Promise<void>;
    execute<P extends A1<T["execute"]>>(a1: P[0]): Promise<void>;
    execute<P extends A2<T["execute"]>>(a1: P[0], a2: P[1]): Promise<void>;
    execute<P extends A3<T["execute"]>>(a1: P[0], a2: P[1], a3: P[2]): Promise<void>;
    execute(...args: any[]): Promise<void> {
        return this.useCase.execute(...args);
    }
}

このような結構無理やりな実装を追加することでexecuteも型チェックがある程度効くようになりました。

本来はVariadic Kindsなどがあるとこのようなラッパーの型を定義できるみたいですが、まだTypeScriptにはありません。

詳しくは次にも書かれているので、興味がある/もっといい方法を知ってる人は該当Issueにコメントください。

♻ Improving

use assertOK instead of assert module #341

assertモジュールを単純なUtilに置き換えたことでファイルサイズが小さくなっています。

⬇️ 4kb

Before

Package size: 12.55 KB

After

Package size: 8.27 KB

関連Issueとしてはeventsの依存も取り除くものがあります。

React NativeではwebpackやbrowserifyのようにassertなどのNodeコアAPIが自動でshimに切り替わらないという問題がありました。 またNodeコアAPIとブラウザのshimの乖離している問題も目立ってきたので、eventsassertの依存は外すか明示的なものに変えようとしています。

🆕 New Modules

@almin/react-context #346 #112

@almin/react-contextというReactユーザー向けのモジュールを追加しました。

名前の通りReact 16.3で追加された新しいReact Context APIに対応したモジュールです。

基本的にはReact Context APIと同じですが、<Consumer>の中身が呼ばれるのはAlminのContextが変化したタイミングになっています。

import { Context, StoreGroup } from "almin";
import { createStore } from "@almin/store-test-helper";
import { createReactContext } from "@almin/react-context";
// Create Almin context
const context = new Context({
    // StoreGroup has {a, b, c} state
    store: new StoreGroup({
        // createStore is a test helper that create Store instance of Almin
        // initial state of `a` is { value: "a" }
        a: createStore({ value: "a" }),
        b: createStore({ value: "b" }),
        c: createStore({ value: "c" }),
    })
});
// Create React Context that wrap Almin Context
const { Consumer, Provider } = createReactContext(context);
// Use Provider
class App extends React.Component {
    render() {
        return (
            // You should wrap Consumer with Provider
            <Provider>
                {/* Consumer children props is called when Almin's context is changed */}
                <Consumer>
                    {state => {
                        return <ul>
                            <li>{state.a.value}</li>
                            <li>{state.b.value}</li>
                            <li>{state.c.value}</li>
                        </ul>
                    }}
                </Consumer>
            </Provider>
        );
    }
}

すでにReactとのバインディングにはHOCを使ったalmin-react-containerがあります。 ただ、React Context APIの方がメンテしやすいと思うので、今後どちらかに(公式サポート)絞る可能性があります。

何か意見や問題などがありましたらお知らせください。

この実装ははてなブックマーク検索PWAで使ったものが元となっていて、すでにこちらのアプリも@almin/react-contextに移行しています。

過去の変更

almin@0.16.0

almin@0.16.0ではContextdispatcherがオプショナルになりました。

const context = new Context({
-   dispatcher: new Dispatcher(),
    store: storeGroup
});