Almin 0.17リリース – executeの型付け、新しいReact Contextのサポート
ステート管理ライブラリのalmin@0.17.0をリリースしました。
詳しい変更点は次の記事でも紹介しています。
⭐ Feature
Support context.useCase#execute
typing #342 #107
AlminはTypeScriptを公式にサポートしています。
しかし今まではcontext.useCase(someUseCase).execute(args)
のargs
がany[]
となっていました。
コレを解消するためにAlmin 0.13でcontext.useCase(someUseCase).executor(useCase => useCase.execute(args))
という型がチェックされるためだけのメソッドを追加していました。
しかし、Almin 0.17ではcontext.useCase(someUseCase).execute(args)
も型チェックがほとんどのケースで効くようになりました。
Almin 0.17+では次のケースで型チェックが効くようになっています。
この例ではMyUseCaseA.prototype.execute
にstring
型ではない値を渡すとコンパイルエラーです。
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)
のargs
がsomeUseCase#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にコメントください。
- almin-thinking/04-Conditional-UseCase.ts at master · almin/almin-thinking
- almin/UseCaseExecutor.ts at master · almin/almin
♻ 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の乖離している問題も目立ってきたので、events
やassert
の依存は外すか明示的なものに変えようとしています。
🆕 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
に移行しています。
- モバイル/オフラインでも動作するはてなブックマーク検索のPWAを作った | Web Scratch
- Update to almin and use @almin/react-context by azu · Pull Request #12 · azu/hatebupwa
過去の変更
almin@0.16.0
almin@0.16.0ではContext
のdispatcher
がオプショナルになりました。
const context = new Context({
- dispatcher: new Dispatcher(),
store: storeGroup
});
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。