typedux
Version:
Slightly adjusted Redux (awesome by default) for TS
155 lines (134 loc) • 4.07 kB
text/typescript
import { isNumber, toNumber } from "@3fv/guard"
/**
* Copyright (C) 2019-present, Rimeto, LLC.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* A generic type that cannot be `undefined`.
*/
export type Defined<T> = Exclude<T, undefined>
/**
* `PropChainObjectWrapper` gives TypeScript visibility into the properties of
* an `PropChainType` object at compile-time.
*/
export type PropChainObjectWrapper<S, T> = {
[K in keyof T]-?: PropChainType<S, T[K]>
}
/**
* Data accessor interface to dereference the value of the `TSOCType`.
*/
export interface PropChainDataAccessor<S, T> {
/**
* Data accessor without a default value. If no data exists,
* `undefined` is returned.
*/
<Callback extends PropChainCallback<S, T>>(
callback: Callback
): PropChainOnFinish<S, T, Callback>
}
/**
* `PropChainArrayWrapper` gives TypeScript visibility into the `PropChainType` 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 PropChainArrayWrapper<
S,
T extends PropChainCallback<S, T | number>
> {
length: PropChainType<S, number>
[K: number]: PropChainType<S, T>
}
/**
* Data accessor interface to dereference the value of an `any` type.
* @extends PropChainDataAccessor<any>
*/
export interface PropChainAny<S> extends PropChainDataAccessor<S, any> {
[K: string]: PropChainAny<S> // Enable deep traversal of arbitrary props
}
/**
* `PropChainDataWrapper` selects between `PropChainArrayWrapper`, `PropChainObjectWrapper`, and `PropChainDataAccessor`
* to wrap Arrays, Objects and all other types respectively.
*/
export type PropChainDataWrapper<S, T> = 0 extends 1 & T // Is T any? (https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360)
? PropChainAny<S> & PropChainDataAccessor<S, T>
: T extends any[] // Is T array-like?
? PropChainArrayWrapper<S, T[number]>
: T extends object // Is T object-like?
? PropChainObjectWrapper<S, T>
: PropChainDataAccessor<S, T>
/**
* An object that supports optional chaining
*/
export type PropChainType<S, T> = PropChainDataAccessor<S, T> &
PropChainDataWrapper<S, NonNullable<T>>
export type PropChainCallback<S, T> = (
getter: (state: S) => T,
keyPath: Array<string | number>
) => any
/**
* Callback returned by prop-chain
*/
export type PropChainOnFinish<
S,
T,
Callback extends PropChainCallback<S, T>
> = ReturnType<Callback> extends (...args: infer P) => infer R
? (...args: P) => R
: never
export function continuePropertyChain<
S,
T,
Callback,
DataAccessor
>(
state: S,
data: T,
keyPath: Array<string | number> = []
): PropChainType<S, T> {
keyPath = keyPath || []
// noinspection DuplicatedCode
return new Proxy(
(overrideCallback: PropChainCallback<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
}
const getter = (state: S) =>
keyPath.reduce((value, key, index) => {
return value[resolveKey(value, key, index)]
}, state)
return overrideCallback(getter, keyPath)
},
{
get: (target, key) => {
return continuePropertyChain(state, undefined, [...keyPath, key as any])
}
}
) as PropChainType<S, T>
}
/**
* Start a new prop chain of the type specified
*
* @param state
*/
export function propertyChain<S>(state: S = undefined): PropChainType<S, S> {
return continuePropertyChain<
S,
S,
PropChainCallback<S, S>,
PropChainDataAccessor<S, S>
>(state, state)
}