typedux
Version:
Slightly adjusted Redux (awesome by default) for TS
156 lines (131 loc) • 4.85 kB
text/typescript
import type {State} from "../reducers"
import type {Selector, InferredSelector, SelectorSubscriptionListener} from "./SelectorTypes"
import { isNumber, toNumber } from "@3fv/guard";
import type {ObservableStore} from "../store/ObservableStore"
// export type SelectorChainType<S,T> = Exclude<SelectorChainType<S, T>, SelectorChainDataAccessor<S,T>> & {
// (): Selector<S,T>
// }
/**
* `SelectorChainObjectWrapper` gives TypeScript visibility into the properties of
* an `SelectorChainType` object at compile-time.
*/
type SelectorChainObjectWrapper<S, T> = { [K in keyof T]-?:SelectorChainType<S, T[K]> };
/**
* Data accessor interface to dereference the value of the `TSOCType`.
*/
// export interface SelectorChainDataAccessor<S, T> {
// /**
// * Data accessor without a default value. If no data exists,
// * `undefined` is returned.
// */<Callback extends SelectorChainCallback<S, T>>(callback:Callback):SelectorChainOnFinish<S, T, Callback>;
//
// /**
// * Data accessor with default value.
// */
// // (defaultValue:NonNullable<T>):NonNullable<T>;
// //
// // (nullDefaultValue:T extends null ? null : never):Defined<T>; // Null case
// }
/**
* `SelectorChainArrayWrapper` gives TypeScript visibility into the `SelectorChainType` values of an array
* without exposing Array methods (it is problematic to attempt to invoke methods during
* the course of an optional chain traversal).
*/
export interface SelectorChainArrayWrapper<S, T extends SelectorChainCallback<S, T | number>> {
length:SelectorChainType<S, number>;
[K:number]:SelectorChainType<S, T>;
}
/**
* Data accessor interface to dereference the value of an `any` type.
* @extends SelectorChainDataAccessor<any>
*/
export interface SelectorChainAny<S> extends SelectorChainDataAccessor<S, S> {
[K:string]:SelectorChainAny<S> // Enable deep traversal of arbitrary props
}
/**
* `SelectorChainDataWrapper` selects between `SelectorChainArrayWrapper`, `SelectorChainObjectWrapper`, and `SelectorChainDataAccessor`
* to wrap Arrays, Objects and all other types respectively.
*/
export type SelectorChainDataWrapper<S, T> =
0 extends (1 & T) // Is T any? (https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360)
? (SelectorChainAny<S> & SelectorChainDataAccessor<S, T>)
: T extends any[] // Is T array-like?
? SelectorChainArrayWrapper<S, T[number]>
: T extends object // Is T object-like?
? SelectorChainObjectWrapper<S, T>
: SelectorChainDataAccessor<S, T>
// /**
// * An object that supports optional chaining
// */
export type SelectorChainType<S, T> =
(SelectorChainDataAccessor<S, T>
& SelectorChainDataWrapper<S, NonNullable<T>>)
export type SelectorChainOnFinish<S, T, Callback extends SelectorChainCallback<S, T>> =
ReturnType<Callback> extends ((...args:infer P) => (infer R)) ? ((...args:P) => R) : never
export interface SelectorChainDataAccessor<S,T> {
(): Selector<S,T>
}
export interface SelectorChainCallback<S,T> {
<T>(getter: (state: S) => T, keyPath: Array<string | number>):
SelectorChainDataAccessor<S,T>
}
function continueSelectorChain<
S,
T
>(
store: ObservableStore<any>,
state:S,
data:T,
keyPath:Array<string | number> = []
): SelectorChainType<S,T> {//SelectorChainType<S,T> {
keyPath = keyPath || []
// noinspection DuplicatedCode
const nextSelector = (():Selector<S,T> => {
// TRACK FIRST PROP ACCESS
const firstGet = keyPath.map(() => true)
// CHECK IF KEY SHOULD BE NUMBER
function resolveKey(value, key, index) {
if (firstGet[index]) {
if (Array.isArray(value)) {
const keyNum = toNumber(key)
if (isNumber(keyNum)) {
key = keyPath[index] = keyNum
}
}
firstGet[index] = false
}
return key
}
function getterFn(state:S) {
return keyPath.reduce((value, key, index) => {
return value[resolveKey(value, key, index)]
}, state) as any
}
const getter:Selector<S,T> = Object.assign(getterFn, {
subscribe(
listener: SelectorSubscriptionListener<T>
) {
return store.observe(getterFn, listener)
}
})
return getter as Selector<S,T> // overrideCallback(getter, keyPath)
}) as SelectorChainDataAccessor<S,T>
return (new Proxy(
nextSelector,
{
get: (target, key) => {
return continueSelectorChain(store,state, undefined, [...keyPath, key as any])
}
}
)) as SelectorChainType<S, T>
}
export type SelectorChain<S> = SelectorChainType<S, S>
//PropChainType<S, S,SelectorChainDataAccessor<S,S>>
export function selectorChain<
S extends State
>(
store: ObservableStore<S>,
state:S
): SelectorChain<S> {
return continueSelectorChain<S,S>(store, state, state)
}