tiny-atom
tiny-atom copied to clipboard
Typescript typings
Hi, I wrote some (WIP) typescript typings which can properly handle tiny-atom with react hook bindings:
declare module 'tiny-atom' {
/** Type to create object like { foo: never, bar: "bar" } if bar matches condition and foo not */
type FilterKeys<Base, Condition> = { [Key in keyof Base]: Base[Key] extends Condition ? Key : never };
/** Return type containing distinction of all values of T */
type Values<T> = T[keyof T];
export interface DispatchFunction<A = any> {
<AA extends ActionsWithoutPayload<A>>(action: AA): void;
<AA extends ActionsWithPayload<A>>(action: AA, payload: ActionHandlerPayload<A[AA]>): void;
}
export type ObserveFunction<ATOM> = (atom: ATOM) => void;
export type UnobserveFunction = () => void;
export type AtomState<ATOM> = ATOM extends Atom<infer S, any> ? S : never;
export type AtomActions<ATOM> = ATOM extends Atom<any, infer A> ? A : never;
export type ActionHandlerPayload<F> =
F extends ActionHandlerWithoutPayload<any> ? undefined :
F extends ActionHandlerWithPayload<any, infer P> ? P :
never;
export type AtomBoundAction<F> =
F extends ActionHandler<any, undefined> ? () => void :
F extends ActionHandler<any, infer P> ? (payload: P) => void :
never;
export type ActionsWithoutPayload<A> = Values<FilterKeys<A, ActionHandlerWithoutPayload>>;
export type ActionsWithPayload<A> = Values<FilterKeys<A, ActionHandlerWithPayload>>;
export type AtomBoundActions<ATOM> = {
[key in keyof AtomActions<ATOM>]: AtomBoundAction<AtomActions<ATOM>[key]>;
};
export interface Atom<S = any, A = any> {
dispatch: DispatchFunction;
fuse: <S2, A2>(state: S2, actions: A2) => Atom<S & S2, A & A2>;
get: () => S;
observe: (cb: ObserveFunction<this>) => UnobserveFunction;
set: (partial: Partial<S>) => void;
swap: (state: S) => void;
}
export interface ActionAtom<S> {
dispatch: DispatchFunction;
get: () => S;
set: (partial: Partial<S>) => void;
swap: (state: S) => void;
}
type ActionHandlerWithoutPayload<S = any> = (store: ActionAtom<S>) => any;
type ActionHandlerWithPayload<S = any, P = any> = (store: ActionAtom<S>, payload: P) => void;
export type ActionHandler<S = any, P = any> = ActionHandlerWithPayload<S, P> | ActionHandlerWithoutPayload<S>;
export default function createAtom<S = {}, A = {}>(initialState: S, actions: A): Atom<S, A>;
}
declare module 'tiny-atom/react' {
import React from 'react';
import { Atom } from 'tiny-atom';
export const Provider: React.SFC<{ atom: Atom }>;
export const Consumer: React.Consumer<Atom>;
export function connect(...args: any[]): any;
}
declare module 'tiny-atom/react/hooks' {
import {
Atom,
AtomState,
AtomActions,
AtomBoundActions,
DispatchFunction,
} from 'tiny-atom';
export interface UseAtomOptions {
/**
* If the connection is pure, the mapped props are compared to previously mapped props for avoiding rerenders.
* Set this to false to rerender on any state change.
* default: true
*/
pure?: boolean;
/**
* By default, the change listeners are debounced such that at most one render occurs per frame.
* Set to true to rerender immediately on change state.
* default: false
*/
sync?: boolean;
/**
* Use this to control if the connector subscribes to the store or simply projects the state on parent rerenders.
* default: true in the browser, false on the server
*/
observe?: boolean;
}
// export function useAtom<ATOM extends Atom, MAP extends (state: AtomState<ATOM>) => any = (state: AtomState<ATOM>) => any>(map?: MAP, options?: UseAtomOptions): ReturnType<MAP>;
// export function useAtom<MAP extends (state: any) => any>(map?: MAP, options?: UseAtomOptions): ReturnType<MAP>;
export function useAtom<S, R>(map?: (state: S) => R, options?: UseAtomOptions): R;
export function useActions<ATOM extends Atom>(): AtomBoundActions<ATOM>;
export function useDispatch<ATOM extends Atom>(): DispatchFunction<AtomActions<ATOM>>;
}
Here's an example of them being used:
import React from 'react';
import createAtom, { ActionAtom, AtomState } from 'tiny-atom';
import { Provider } from 'tiny-atom/react';
import { useAtom, useActions, useDispatch } from 'tiny-atom/react/hooks';
interface State {
count: number;
}
const initialState: State = {
count: 0,
};
const atomActions = {
increment: ({ get, set }: ActionAtom<State>, n: number) => {
const count = get().count;
set({ count: count + n });
},
decrement: ({ get, set }: ActionAtom<State>, n: number) => {
const count = get().count;
set({ count: count - n });
},
/** payload-less action */
incrementOne: ({ get, set }: ActionAtom<State>) => {
set({ count: get().count + 1 });
},
log: ({ }: ActionAtom<State>, msg: string) => {
console.log(msg);
},
};
const atom = createAtom(initialState, atomActions);
/** utility function to simplyfy useAtom calls */
function useAtomTyped<ATOM>() {
return <MAP extends (state: AtomState<ATOM>) => any>(map?: MAP) =>
useAtom<AtomState<ATOM>, ReturnType<MAP>>(map);
}
const TestTinyAtom = () => {
// const state = useAtom((s: AtomState<typeof atom>) => s.count);
const state = useAtomTyped<typeof atom>()(s => s.count);
const actions = useActions<typeof atom>();
const dispatch = useDispatch<typeof atom>();
// even correctly handles dispatch ('incrementOne') needing no args and dispatch ('increment', 1) requiring a number argument
// so for example dispatch('increment') or dispatch('decrement', 'test') will correctly throw build errors! ;-)
return (
<div>
<p>Counter = {state}</p>
<button onClick={() => actions.incrementOne()}>+</button>
<button onClick={() => actions.decrement(1)}>-</button>
<button onClick={() => dispatch('decrement', 2)}>--</button>
</div>
);
};
export default () => (
<Provider atom={atom}>
<TestTinyAtom />
</Provider>
);
I think it might be worth including typings in this project as it greatly improves usability. Also it properly handles payload-less dispatch calls which was quite difficult to figure out 😉
These are still not finished completely (connect missing and some other small stuff for createAtom), but I want to post these here for now so others looking for this library can use them already.
Uuu, this is awesome, will have a play soon.
Does this autocomplete the available actions and their signatures in useActions
or when calling them in the code? And does it do it for dispatch
too?
Sure ;-)
It works properly for actions, but it's not possible for dispatch because it works through overloads.