Skip to content

Commit

Permalink
Make enums callable
Browse files Browse the repository at this point in the history
Refactors enum to enable calling like a factory function.
  • Loading branch information
texastoland committed Mar 27, 2022
1 parent 6c1d270 commit 7b7df53
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 71 deletions.
60 changes: 26 additions & 34 deletions src/deprecated.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,34 @@
import { Counter, enumOf, Integers, Lowercased, Strings } from './enum-xyz.js'
import { enumOf, Integers, Lowercased, Strings } from './enum-xyz.js'

/** @type {import("./types").deprecated} */
const deprecated = ({ from, to, using }) =>
enumOf(
(name) => (
console.warn(`\`${from}\` is deprecated; use \`${to}\` instead`),
using[name]
)
)
/**
* @template T
* @param {import("./types").Enum<T>} replacement
* @param {string} warning
*/
const deprecated = (replacement, warning) =>
enumOf((name) => (console.warn(warning), replacement[name]))

/** @deprecated */
export const String = deprecated({
from: 'String',
to: 'Strings',
using: Strings,
})

export const String = deprecated(
Strings,
'`String` is deprecated; use `Strings` (plural) instead'
)
/** @deprecated */
export const StringLower = deprecated({
from: 'StringLower',
to: 'Lowercased',
using: Lowercased,
})

export const StringLower = deprecated(
Lowercased,
'`StringLower` is deprecated; use `Lowercased` instead'
)
/** @deprecated */
export const Numeric = deprecated({
from: 'Numeric',
to: 'Integers',
using: Integers,
})

export const Numeric = deprecated(
Integers,
'`Numeric` is deprecated; use `Integers` instead'
)
/**
* @deprecated
* @param {number} startIndex
* @param {number} start
*/
export const NumericAt = (startIndex) =>
deprecated({
from: 'NumericAt',
to: 'Counter',
using: Counter(startIndex),
})
export const NumericAt = (start) =>
deprecated(
Integers(start),
'`NumericAt()` is deprecated; use `Integers()` instead'
)
56 changes: 31 additions & 25 deletions src/enum-xyz.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
/**
* @type {import("./types").enumOf}
* @param mapper should return the same value for the same name
*/
export const enumOf = (mapper, state) =>
new Proxy(
{},
{
// @ts-ignore
get: (_, name) => mapper(name, state),
}
)
/** @type {import("./types").enumOf} */
export const enumOf = (getter) => {
const handler = getter instanceof Function ? { get: getter } : getter,
{ get, apply } = handler
// must target function to be callable
return new Proxy(apply ? () => {} : {}, {
...handler,
get: (_, /** @type {string} */ name) => get(name),
apply:
apply && ((_, _1, /** @type {any} */ args) => apply(handler, ...args)),
})
}

/**
* @type {import("./types").memoEnumOf}
* @param mapper always returns the same value for the same name
*/
export const memoEnumOf = (mapper) =>
enumOf(
(name, map) => map.get(name) ?? map.set(name, mapper(name)).get(name),
new Map()
)
/** @type {import("./types").memoEnumOf} */
export const memoEnumOf = (getter) => {
const handler = getter instanceof Function ? { get: getter } : getter,
cache = new Map()
return enumOf({
...handler,
get: (name) =>
cache.get(name) ?? cache.set(name, handler.get(name)).get(name),
})
}

export const Strings = enumOf((name) => name)

export const Lowercased = enumOf((name) => name.toLowerCase())

export const Symbols = memoEnumOf(Symbol)
export const Symbols = enumOf({
get: Symbol.for,
apply: () => enumOf(Symbol),
})

export const Counter = (startIndex = 0) => memoEnumOf((name) => startIndex++)

export const Integers = Counter()
let nextInteger = 0
export const Integers = memoEnumOf({
get: () => nextInteger++,
apply: (_, start = 0) => enumOf(() => start++),
})
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from './enum-xyz.js'
export * from './deprecated.js'
// don't re-export extras
// re-export types
/** @template T @typedef {import("./types").Enum<T>} Enum */
/** @template T @template {[] | [any]} [TArgs=[]]
@typedef {import("./types").CallableEnum<T, TArgs>} CallableEnum */
/** @template T @typedef {import("./types").Strings<T>} Strings */
/** @template T @typedef {import("./types").Lowercased<T>} Lowercased */
27 changes: 16 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ export type Enum<T, TKey extends string = string> = { [name in TKey]: T }
export type Strings<T extends string> = Enum<T, T>
export type Lowercased<T extends string> = Enum<T, Lowercase<T>>

type MaybeArg = [] | [any]
export type CallableEnum<T, TArgs extends MaybeArg = []> =
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
[TArgs] extends [never] ? Enum<T> : Enum<T> & ((...args: TArgs) => Enum<T>)

// enum-xyz.js
export type enumOf = <T, TState = never>(
mapper: (name: string, state: TState) => T,
state?: TState
) => Enum<T>
export type memoEnumOf = <T>(mapper: (name: string) => T) => Enum<T>
type Handler<T, TArgs extends MaybeArg> = {
get: (name: string) => T
apply?: (handler: Handler<T, TArgs>, ...args: TArgs) => Enum<T>
} & Omit<ProxyHandler<any>, 'get' | 'apply'>

export type enumOf = <T, TArgs extends MaybeArg = never>(
handler: Handler<T, TArgs> | Handler<T, TArgs>['get']
) => CallableEnum<T, TArgs>

// deprecated.js
export type deprecated = <T>(props: {
from: string
to: string
using: Enum<T>
}) => Enum<T>
export type memoEnumOf = <T, TArgs extends MaybeArg = never>(
handler: Handler<T, TArgs> | Handler<T, TArgs>['get']
) => CallableEnum<T, TArgs>

// extras.js
export type actionCreator<TType extends string = string> = {
Expand Down

0 comments on commit 7b7df53

Please sign in to comment.