BlaC is written in TypeScript and assumes you are too. Almost everything you need — state shape, action signatures, the args a consumer must pass — flows from a single class declaration, so most of this page is about reading the inference rather than writing annotations.
This is the discoverable, example-driven tour. For the exhaustive list of exported type utilities (ExtractState, ExtractArgs, ExtractDeps, InstanceReadonlyState, and friends), see Core Types.
"experimentalDecorators":true,// only if you use @blac(...) decorator syntax
},
}
strict: true is the assumption behind every example below — strictNullChecks in particular is what makes this.args correctly Args | undefined and what forces you to narrow discriminated unions.
Decorators are optional. @blac(...) works as either a legacy (experimentalDecorators) decorator or a TC39 stage-3 decorator. If you’d rather not touch decorator flags, blac(opts)(class) is a plain higher-order function the decorator form is sugar over — see Configuration for the full options union and both call forms.
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Override of StructuralContainer.patch that routes through the
StateContainer concerns: disposed guard, dev-only emit-rate check,
_changedWhileHydrating flag, pending-change capture (so stateChanged
system events see the merged prev/next), and the registry-level
stateChanged notification. We still call
super.patch so path-marking semantics (the whole point of patch) are
preserved.
patch({
count?:number|undefined
count:2});// patch takes DeepPartial<S>
}
}
emit and update’s returned value both require the completeS — omit a key and it’s a type error, which is the compiler enforcing the replace-not-merge rule. patch accepts a DeepPartial<S>, so partial objects are legal there and only there.
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
}[])=>number,initialValue: number): number (+2overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
@param ― callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.
@param ― initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
reduce((
sum:number
sum,
i:{
price:number;
qty:number;
}
i)=>
sum:number
sum+
i:{
price:number;
qty:number;
}
i.
price:number
price*
i:{
price:number;
qty:number;
}
i.
qty:number
qty,0);
}
get
CartCubit.isEmpty: boolean
isEmpty(){
// return type inferred as boolean — no annotation needed
returnthis.
StructuralContainer<CartState>.state: CartState
state.
CartState.items: {
price:number;
qty: number;
}[]
items.
Array<{price:number;qty:number;}>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length===0;
}
}
Reading cart.total in render is reactive without any type-level support — see Dependency Tracking for the runtime rule.
Model a request as a status tag and the compiler forces you to handle every case and forbids reading a field before it exists — because emit wants the full variant, you can’t emit { status: 'success' } without the fields that variant requires:
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String(
function(localvar)e: unknown
e) });
}
};
}
declarefunction
functionfetchUser(id: string): Promise<User>
fetchUser(
id:string
id: string):
interfacePromise<T>
Represents the completion of an asynchronous operation
Promise<
interfaceUser
User>;
On the read side, switching on state.status (from const [state] = useBloc(UserCubit)) narrows the union — state.user exists only in the success arm, state.error only in error, ordinary TypeScript control-flow narrowing with no BlaC-specific machinery involved. Narrowing works identically inside a getter — derive a flag once and read it everywhere. Give a switch’s default an assignment to never for exhaustiveness checking (add a variant later and forget a case, and the assignment fails to compile).
select lets a consumer use an explicit dependency array instead of auto-tracked reads. The component re-renders only when one of those values changes (compared per-index with Object.is). Its signature is:
Both arguments are fully inferred from the bloc you pass to useBloc — state is the readonly state, bloc is the readonly instance (so getters are reachable):
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
}[])=>number,initialValue: number): number (+2overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
@param ― callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.
@param ― initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
reduce((
s:number
s,
i:{
price:number;
qty:number;
}
i)=>
s:number
s+
i:{
price:number;
qty:number;
}
i.
price:number
price*
i:{
price:number;
qty:number;
}
i.
qty:number
qty,0);
}
}
function
functioncartTotal(): number
cartTotal(): number {
// re-renders only when `total` or item count changes
React hook that connects a component to a state container with automatic
re-render on state changes.
Two tracking modes:
Auto-tracking (default): the returned state value is a proxy that
records read paths during render. The component re-renders when any
recorded path changes. Backed by @dirtytalk/structural's
trackRender
the container's path-scoped DirtyChannel.
Manual select: pass options.select to opt out of auto-tracking.
The hook re-renders only when the returned array's elements change
(per-index Object.is).
Lifecycle:
The bloc is acquired from the registry on mount and released on
unmount. The instance key is derived from options.args (own args),
then the surrounding
BlocProvider
context args for this bloc,
then the default key (no args).
options.onMount fires after the bloc is acquired; options.onUnmount
fires before the registry releases its ref, so the bloc is still
alive when the callback runs.
Per-consumer re-render selector. When provided, the hook re-renders
only when the returned array's elements change (Object.is per index).
When omitted, auto-tracking is used: any state path read during the
render is observed, and the hook re-renders when any of those paths
change.
Keep the selector referentially stable across renders (e.g. via
useCallback) — passing a fresh function each render forces the
subscription to re-key, which the underlying channel treats as a new
consumer.
select:(
state:Readonly<CartState>
state,bloc)=> [bloc.
total:number
total,
state:Readonly<CartState>
state.
items:{
price:number;
qty:number;
}[]
items.
Array<{price:number;qty:number;}>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length],
bloc:InstanceReadonlyState<typeofCartCubit>
});
return
constcart: InstanceReadonlyState<typeofCartCubit>
cart.
total:number
total;
}
state is Readonly, so select can’t accidentally mutate it; that’s also why reading a getter here (bloc.total) is the supported way to make a derived value drive re-renders. Keep the selector referentially stable — a fresh function each render re-keys the subscription. See useBloc.
When a bloc declares an Args type, args in useBloc is optional — omit it and the hook inherits args from a <BlocProvider> ancestor, or falls back to the default instance key. When the bloc’s Args is the default void, passing args is forbidden. A conditional option type enforces both directions:
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Cubit<{
count:number
count: number }>{
constructor(){
super({
count:number
count:0});
}
}
function
functioncount(): number
count(): number {
// CounterCubit has no Args → passing `args` is a type error
React hook that connects a component to a state container with automatic
re-render on state changes.
Two tracking modes:
Auto-tracking (default): the returned state value is a proxy that
records read paths during render. The component re-renders when any
recorded path changes. Backed by @dirtytalk/structural's
trackRender
the container's path-scoped DirtyChannel.
Manual select: pass options.select to opt out of auto-tracking.
The hook re-renders only when the returned array's elements change
(per-index Object.is).
Lifecycle:
The bloc is acquired from the registry on mount and released on
unmount. The instance key is derived from options.args (own args),
then the surrounding
BlocProvider
context args for this bloc,
then the default key (no args).
options.onMount fires after the bloc is acquired; options.onUnmount
fires before the registry releases its ref, so the bloc is still
alive when the callback runs.
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Cubit<
interfaceUserState
UserState,{
userId:string
userId: string }>{
constructor(){
super({
UserState.name: string
name:''});
}
protected
UserCubit.init(args: {
userId:string;
}): void
Called once after construction with the args passed at acquire time, before the first
state snapshot is read by any consumer. Override to seed args-derived state (via
this.emit(...)) or kick off loads.
init(
args:{
userId:string;
}
args:{
userId:string
userId: string }){
void
args:{
userId:string;
}
args.
userId:string
userId;
}
}
function
functionuserName(): string
userName(): string {
// args is optional — omit to inherit from BlocProvider, or pass explicitly:
React hook that connects a component to a state container with automatic
re-render on state changes.
Two tracking modes:
Auto-tracking (default): the returned state value is a proxy that
records read paths during render. The component re-renders when any
recorded path changes. Backed by @dirtytalk/structural's
trackRender
the container's path-scoped DirtyChannel.
Manual select: pass options.select to opt out of auto-tracking.
The hook re-renders only when the returned array's elements change
(per-index Object.is).
Lifecycle:
The bloc is acquired from the registry on mount and released on
unmount. The instance key is derived from options.args (own args),
then the surrounding
BlocProvider
context args for this bloc,
then the default key (no args).
options.onMount fires after the bloc is acquired; options.onUnmount
fires before the registry releases its ref, so the bloc is still
alive when the callback runs.
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Cubit<{
name:string
name: string },{
userId:string
userId: string }>{
constructor(){
super({
name:string
name:''});
}
protected
UserCubit.init(args: {
userId:string;
}): void
Called once after construction with the args passed at acquire time, before the first
state snapshot is read by any consumer. Override to seed args-derived state (via
this.emit(...)) or kick off loads.
init(
args:{
userId:string;
}
args:{
userId:string
userId: string }){
// `args` here is the non-optional declared shape
voidthis.
UserCubit.fetch(_id: string): Promise<void>
fetch(
args:{
userId:string;
}
args.
userId:string
userId);
}
UserCubit.retry(): void
retry(){
// the `args` GETTER is `Args | undefined` — guard it
Wrapping useBloc in a domain hook is the idiomatic way to give a feature a named, typed entry point. Inference is preserved end to end as long as you don’t widen the return:
Cubit<S> is a StateContainer<S> with emit / patch exposed as
public mutation surface. Today it adds nothing structurally beyond
StateContainer — both are inherited from the underlying
StructuralContainer<S>. Kept as a real class (not a type alias) because
downstream code does instance instanceof Cubit checks.
The class body is intentionally empty: a no-op emit override would
still go through applyState, and patch is inherited from
StructuralContainer (path-diffed, microtask-flushed). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
Override of StructuralContainer.patch that routes through the
StateContainer concerns: disposed guard, dev-only emit-rate check,
_changedWhileHydrating flag, pending-change capture (so stateChanged
system events see the merged prev/next), and the registry-level
stateChanged notification. We still call
super.patch so path-marking semantics (the whole point of patch) are
preserved.
patch({
items?:readonlystring[] |undefined
items: [...this.
StructuralContainer<TodoState>.state: TodoState
state.
TodoState.items: string[]
items,
t:string
t] });
get
TodoCubit.count: number
count(){
returnthis.
StructuralContainer<TodoState>.state: TodoState
state.
TodoState.items: string[]
items.
Array<string>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length;
}
}
// no explicit return annotation needed — it's inferred
React hook that connects a component to a state container with automatic
re-render on state changes.
Two tracking modes:
Auto-tracking (default): the returned state value is a proxy that
records read paths during render. The component re-renders when any
recorded path changes. Backed by @dirtytalk/structural's
trackRender
the container's path-scoped DirtyChannel.
Manual select: pass options.select to opt out of auto-tracking.
The hook re-renders only when the returned array's elements change
(per-index Object.is).
Lifecycle:
The bloc is acquired from the registry on mount and released on
unmount. The instance key is derived from options.args (own args),
then the surrounding
BlocProvider
context args for this bloc,
then the default key (no args).
options.onMount fires after the bloc is acquired; options.onUnmount
fires before the registry releases its ref, so the bloc is still
alive when the callback runs.
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length;
}
If you do want to annotate the return — for a public package API, say — derive it from the bloc with ExtractState/InstanceReadonlyState rather than restating the shape by hand; both are exported from @blac/core and documented in Core Types.