Skip to content

Instantly share code, notes, and snippets.

@ackvf
Last active January 29, 2026 15:45
Show Gist options
  • Select an option

  • Save ackvf/68f992660a5eda645c4671d3599b2acf to your computer and use it in GitHub Desktop.

Select an option

Save ackvf/68f992660a5eda645c4671d3599b2acf to your computer and use it in GitHub Desktop.
TS/JS utility functions

TS/JS utility functions

other gists
🔗 TypeScript type toolbelt
🔗 React utils

note: Any ambient types referenced in the code samples below are defined in global.d.ts from the TypeScript type toolbelt.


  • accessTrap - Object and Array Proxy to count number of property accesses to map used/unused properties. It maintains referential stability using a caching mechanism to not disrupt React.js render flow.

    image

  • colorizer - Object with composable console coloring primitives. Optional support for tagged templates.

    console.log(c.fg.black + c.bg.red + 'Is this thing on?' + c.reset) // compose colors
    const dyeRed = str => c.fg.black + c.bg.red + str + c.reset // reuseable
    console.log(dyeRed(error))
  • proxy colorizer - An extension to the console coloring object using proxy with cleaner syntax and chaining.

    const dyeRed = _.bg.red.fg.black // chainable form
    console.log(dyeRed + 'Is this thing on?' + _.reset) // compose with concatenation
    console.log(dyeRed('Is this thing on?')) // as function
    console.log(dyeRed`Is this ${thing} on?`) // as a tagged template
  • withProxyFallback - A proxy wrapper that returns a predefined fallback value or prop for undefined properties instead of undefined.

    const safeObj = withProxyFallback("a", { a: 1 })
    safeObj.b // 1 (falls back to default property "a")
  • sleep / wait - Promises to create a delay.

  • debounce - A cancelable extension to a debounce function.

  • debounce Promise - An impractical thought experiment.

  • smash - Smash that button! - Execute a callback after being called a certain number of times within an optional cooldown period. Bonus: Combine with debounce to limit the "smash" rate.

  • poll - Returns a promise that keeps calling a given function in set intervals until it returns a value for the first time.

  • slugify / getNodeText (React) - Create URL#anchor-friendly text from a JSX component or any text.

 

Learning 

/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-accesstrap-ts)
*
* @description
* This helper wraps an object or an array with a Proxy allowing to reveal and count the number of accesses to each property.
*
* @example
* const counter = {}
* const x = accessTrap({address: { city: "" }}, counter, { verbose: true })
* x.address === x.address // true
*
* @example
* // • Restart counter for each render
* const DropMintWrapper: React.FC<DropMintWrapperProps> = ({ drop }) => {
* const c = window.d.c1 = {}
* drop = accessTrap(drop, c, { verbose: true })
* return <DropMintTemplate drop={drop} />
* }
*
* @example
* // • Global counter for all renders
* // • Map accessTrap in an array for cumulative counting (optional) or apply it to an array
* const c = window.d.c2 = {} // puts the collector object in the window
* const HomePage: NextPage<HomePageProps> = ({ drops }) => {
* drops = drops!.map(drop => accessTrap(drop, c))
* return <DiscoverPageGridFeatured drops={drops} />
* }
*/
export function accessTrap<T extends AnyObject>(target: T, collector: AnyObject): T
export function accessTrap<T extends AnyObject>(target: T, collector: AnyObject, settings?: Settings): T
export function accessTrap<T extends AnyObject>(this: Trap, target: T, __collector: AnyObject, { verbose = false }: Settings = {}): T {
const trap: Trap = {
get,
__collector,
__cache: {},
__previous: this?.__previous ?? '',
__settings: {
verbose,
},
}
if (typeof __collector !== 'object' || __collector === null) throw new Error('Collector must be an object.')
return new Proxy(target, trap) as T
}
function get(this: Trap, target: AnyObject, prop: string): any {
const __previous = this.__previous + (this.__previous && '.')
if (this.__settings.verbose) console.log(`👮‍♂️ ${Y}Accessing property ${this.__previous ? `${B}${__previous}` : ''}${C}${prop}${!isNaN(this.__collector[prop]) ? ` ${B}(${this.__collector[prop] + 1}x)` : ''}${this.__cache[prop] ? ` ${M}(cached)` : ''} %O🔍`, target)
const value = target[prop]
if (typeof value === 'object' && value !== null) {
this.__collector[prop] ??= {}
return this.__cache[prop] ??= accessTrap.bind({ __previous: __previous + prop })(value, this.__collector[prop], this.__settings)
}
this.__collector[prop] ??= 0
this.__collector[prop]++
return value
}
interface Trap {
/* list of available traps https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy#handler_functions */
get(target: AnyObject, prop: string): any
/** Object that stores the number of accesses. */
__collector: AnyObject
/** Caches proxyfied props to avoid reference change. */
__cache: AnyObject
/** The previous access path. */
__previous: string
__settings: Settings
}
interface Settings {
/** Log each access to console. */
verbose?: boolean
}
const B = '\x1b[34m'
const C = '\x1b[36m'
const M = '\x1b[35m'
const Y = '\x1b[33m'
const colorCodes = {
/** Strips ANSI color codes from a string. */
uncolorize: (str: string) => str.replace(/(\x1b|)\[[\d;]*m/gi, ''), // eslint-disable-line no-control-regex
reset: '0',
bright: '1',
dim: '2',
underscore: '4',
blink: '5',
reverse: '7',
hidden: '8',
fg: {
black: '30',
red: '31',
green: '32',
yellow: '33',
blue: '34',
magenta: '35',
cyan: '36',
white: '37',
crimson: '38',
},
bg: {
black: '40',
red: '41',
green: '42',
yellow: '43',
blue: '44',
magenta: '45',
cyan: '46',
white: '47',
crimson: '48',
},
/** e.g. `fold('0;30;41')` => `\x1b[0;30;41m` */
fold(codeString: CodeString) { return `\x1b[${codeString as string}m` as const },
} as const
/* Proxy implementation */
const trap = {
get(target: Secret & ProxyTarget, prop: ColorizerKeys & ProxyHandler<ProxyTarget>, receiver: typeof target) {
// Proxify own properties
if (Object.hasOwn(target, prop)) {
let _code: CodeString = target._code ?? ''
let nextTarget: ProxyTarget
let callable: CallableTaggedTemplateProxy | object
if (['fg', 'bg'].includes(prop)) {
nextTarget = colorCodes[prop as 'fg' | 'bg']
callable = {}
} else {
nextTarget = colorCodes
_code += (_code ? ';' : '') + (target as ColorCodes)[prop as keyof ColorCodes]
callable = function callableTaggedTemplate(...message: Args): string {
// If a message is provided, wrap it between `color` and `reset` codes -> _.bg.red`message` -> \x1b[41mmessage\x1b[0m
// or return just the color code to allow manual composition. -> _.bg.red + myString + _.reset -> \x1b[41m + myString + \x1b[0m
return colorCodes.fold(_code) + (message.length ? x(message) + colorCodes.fold(colorCodes.reset) : '')
}
}
const newTarget = Object.assign(callable, nextTarget, { _code })
return new Proxy(newTarget, trap)
}
// Override default behavior
if (['toString', 'valueOf', Symbol.toPrimitive].includes(prop))
return () => colorCodes.fold(target._code)
// Support string methods directly on the proxy
if (String.prototype[prop as keyof string] !== undefined)
return String.prototype[prop as keyof string]
// Loop back on unknown properties, e.g. _.bg.unknown.fg.black.yellow.fg.dim.red.bright.white.underscore => _.bg.black.fg.red.bright.underscore
// Explanation: after .bg it loops until receiving a valid color name, same after .black where it expects root level property, etc.
return receiver
},
set() { console.warn("Proxy Colorizer: Assignment isn't supported."); return true }, // Silently ignore sets
}
/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-colorizer-proxy-ts)
*
* @description An extension to the console coloring object using proxy with cleaner syntax, chaining, tagged template literal support & customizations.
*
* note:
* - `\x1b` = ``
* - joining supported: `\x1b[0;30;41m`
*
* @example
*
* const red = colorProxy.fg.red
* const dyeRed = colorProxy.fg.black.bg.red
*
* console.log(red`Hallo?`)
* console.log(dyeRed('ERROR:'))
*
* logger.log(`${colors.fg.yellow}API server is listening on http://localhost:${red(port)+colors.fg.yellow}/api/v1/...${colors.reset}`)
*
* @example // All these are equivalent.
*
* console.log(colorProxy.fg.black.bg.red + 'Is this thing on?' + colors.reset + ' ...')
* console.log(colorProxy.fg.black.bg.red('Is this thing on?') + ' ...')
* console.log(colorProxy.fg.black.bg.red`Is this thing on?` + ' ...')
*
* @example
* console.log('\x1b[32;41m green-red')
* console.log(' green-red')
* console.log(c.uncolorize(' default'))
*/
export const colorProxy = new Proxy<ColorProxy>(colorCodes as ColorProxy, trap as ProxyHandler<ColorProxy>)
export default colorProxy
/* Support for template literals */
function interlace(strs: TemplateStringsArray | TemplateStringsArray['raw'], ...args: TextLike[]): string {
return strs.reduce((prev, current, ix) => prev + (current ?? '') + (args[ix] ?? ''), '')
}
const x = extractMessage
function extractMessage(args: Args): string {
if (!['string', 'number', 'boolean'].includes(typeof args[0]) && !Array.isArray(args[0])) return ''
if (isTemplate(args)) return interlace(...args)
return `${args.join(' ') ?? ''}`
}
function isTemplate(args: Args): args is TemplateArgs { return !!(args as TemplateArgs)?.[0]?.raw }
type Args =
| /** e.g. green('yes') */ MsgArgs
| /** e.g. red`no` */ TemplateArgs
/** When called as a tagged template: `` red`nope` `` */
type TemplateArgs = [template: TemplateStringsArray, ...values: TextLike[]]
/** When called as a function: `` green('yes') `` */
type MsgArgs = [message: TextLike]
type TextLike = string | number | boolean
/* Support for Proxy */
export type ColorProxy =
& Secret
& Utils
& Modifiers
& {
fg: Secret & { [key in keyof ColorCodes['fg']]: CallableTaggedTemplateProxy }
bg: Secret & { [key in keyof ColorCodes['bg']]: CallableTaggedTemplateProxy }
}
type CallableTaggedTemplateProxy = string & ColorProxy & ((...msg: Args) => string)
type ColorCodes = typeof colorCodes
type Secret = { _code: CodeString }
type Utils = Pick<ColorCodes, 'uncolorize' | 'fold'>
type Modifiers = { [key in Extract<keyof ColorCodes, 'reset' | 'bright' | 'dim' | 'underscore' | 'blink' | 'reverse' | 'hidden'>]: CallableTaggedTemplateProxy }
type CodeString = `${number}${`;${number}${`;${number}${string}` | ''}` | ''}` | ''
type ProxyTarget = ColorCodes | ColorCodes['fg' | 'bg']
type ColorizerKeys = keyof ColorCodes | keyof ColorCodes['bg' | 'fg']
// ---------------------------------------------------------------------------------------------------------------------
/**
* ## Convenience methods
*
* - as functions `` red(greet + "world") ``
* - as tagged templates `` red`${greet} world` ``
* - chainable `` dyeRed = _.bg.red.fg.black ``
* - composable `` _.bg.red + _.fg.black + "hello" + _.reset `` or `` bgRed + fgBlack + "hello" + _.reset ``
*/
export const _ = colorProxy
export const dyeRed = _.fg.black.bg.red
export const dyeGreen = _.fg.black.bg.green
export const dyeBlue = _.fg.black.bg.blue
export const red = _.fg.red
export const green = _.fg.green
export const blue = _.fg.blue
export const r = _.fg.red
export const g = _.fg.green
export const b = _.fg.blue
export const c = _.fg.cyan
export const m = _.fg.magenta
export const y = _.fg.yellow
export const k = _.fg.black
export const w = _.fg.white
export const R = _.fg.black.bg.red
export const G = _.fg.black.bg.green
export const B = _.fg.black.bg.blue
export const C = _.fg.black.bg.cyan
export const M = _.fg.black.bg.magenta
export const Y = _.fg.black.bg.yellow
export const W = _.fg.black.bg.white
/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-colorizer-ts)
*
* @description Primitive colorizing "library" with template literal support & customizations.
*
* note:
* - `\x1b` = ``
* - joining supported: `\x1b[0;30;41m`
*
* @example
* console.log(colors.bg.red + colors.fg.black + 'Is this thing on?' + colors.reset)
* console.log(dyeRed(error)) // as function
* console.log(red`nope`) // as a tagged template
*
* logger.log(`${colors.fg.yellow}API server is listening on http://localhost:${red(port)+colors.fg.yellow}/api/v1/...${colors.reset}`)
*
* @example
* // Using codes manually
* console.log('\x1b[32;41m green-red')
* console.log(' green-red')
* console.log(colors.uncolorize(' default'))
*/
export const colors = {
uncolorize: (str: string) => str.replace(/(\x1b|)\[[\d;]*m/gi, ''),
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
underscore: '\x1b[4m',
blink: '\x1b[5m',
reverse: '\x1b[7m',
hidden: '\x1b[8m',
fg: {
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
crimson: '\x1b[38m',
},
bg: {
black: '\x1b[40m',
red: '\x1b[41m',
green: '\x1b[42m',
yellow: '\x1b[43m',
blue: '\x1b[44m',
magenta: '\x1b[45m',
cyan: '\x1b[46m',
white: '\x1b[47m',
crimson: '\x1b[48m',
},
}
/* Support for tagged templates */
export function extractMessage(args: Args): string {
if (isTemplate(args)) return interlace(...args)
return `${args[0]}`
}
function interlace(strs: TemplateStringsArray, ...args: any[]): string {
return strs.reduce((prev, current = '', ix) => prev + current + (args[ix] ?? ''), '')
}
function isTemplate(args: Args): args is TemplateArgs { return !!args[0].raw }
type TemplateArgs = [template: TemplateStringsArray, ...values: any[]]
type Args =
| /* as a function: red('hello') */ [message: any]
| /* as tagged template: red`hi` */ TemplateArgs
// --------------------------------------------------------------------------------------------------------------------
/**
* Thanks to this function, you can use the examples below and create your own
* - as functions `` red(greet + "world") ``
* - as tagged templates `` red`${greet} world` ``
*/
const _ = extractMessage
/* Shorthand examples */
export const red /*****/ = (...message: Args): string => colors.fg.red + _(message) + colors.reset
export const green /*****/ = (...message: Args): string => colors.fg.green + _(message) + colors.reset
export const blue /*****/ = (...message: Args): string => colors.fg.blue + _(message) + colors.reset
export const dyeRed /**/ = (...message: Args): string => colors.fg.black + colors.bg.red + _(message) + colors.reset
export const dyeGreen /**/ = (...message: Args): string => colors.fg.black + colors.bg.green + _(message) + colors.reset
export const dyeBlue /**/ = (...message: Args): string => colors.fg.black + colors.bg.blue + _(message) + colors.reset
/* More predefined text colors */
export const r = (...message: Args): string => colors.fg.red + _(message) + colors.reset
export const g = (...message: Args): string => colors.fg.green + _(message) + colors.reset
export const b = (...message: Args): string => colors.fg.blue + _(message) + colors.reset
export const c = (...message: Args): string => colors.fg.cyan + _(message) + colors.reset
export const m = (...message: Args): string => colors.fg.magenta + _(message) + colors.reset
export const y = (...message: Args): string => colors.fg.yellow + _(message) + colors.reset
/* More predefined colors with inverted scheme */
export const R = (...message: Args): string => colors.fg.black + colors.bg.red + _(message) + colors.reset
export const G = (...message: Args): string => colors.fg.black + colors.bg.green + _(message) + colors.reset
export const B = (...message: Args): string => colors.fg.black + colors.bg.blue + _(message) + colors.reset
export const C = (...message: Args): string => colors.fg.black + colors.bg.cyan + _(message) + colors.reset
export const M = (...message: Args): string => colors.fg.black + colors.bg.magenta + _(message) + colors.reset
export const Y = (...message: Args): string => colors.fg.black + colors.bg.yellow + _(message) + colors.reset
export const K = (...message: Args): string => colors.fg.black + colors.bg.white + _(message) + colors.reset
/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-debounce-ts)
*
* @description Returns a function, that, as long as it continues to be invoked, will not
* trigger the callback. The callback will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the callback on the
* leading edge, instead of the trailing.
*
* `.cancel()` can be called manually to cancel the scheduled *trailing* invocation or reset the *leading* cooldown, allowing *immediate* invocation again.
*
* **Note**: The `debounced` output function must be created just once.
*
* @example
* const debounced = debounce((ev) => console.log('once'), 500)
* window.addEventListener('keypress', debounced)
*
* @param wait - [milliseconds]
* @param immediate - *(optional)* Control whether the callback should be triggered on the leading or trailing edge of the wait interval.
*/
export function debounce<Callback extends AnyFunction<void>>(cb: Callback, wait: number, immediate?: boolean) {
let timeout: NodeJS.Timeout | undefined
const later = (self: unknown, args: Parameters<Callback>) => {
timeout = undefined
if (!immediate) cb.apply(self, args)
}
function debounced(this: unknown, ...args: Parameters<Callback>): void {
const callNow = !timeout
if (timeout) clearTimeout(timeout)
timeout = setTimeout(later, wait, this, args)
if (immediate && callNow) cb.apply(this, args)
}
/**
* Call manually to
* - cancel the scheduled ***trailing*** invocation *(`immediate: false`, default)*
* - or reset the ***leading*** cooldown, allowing *immediate* invocation again. *(`immediate: true`)*
*/
debounced.cancel = () => {
if (timeout) {
clearTimeout(timeout)
timeout = undefined
}
}
return debounced
}
type AnyFunction<R = any> = (...args: any[]) => R
/**
* An impractical thought experiment...
*
* @description Returns a promise Executor function (`(resolve, reject) => void`) used to initialize a promise,
* that, as long as it continues to be invoked, will not trigger the `resolve` callback.
* The resolve callback will be called after it stops being called for N milliseconds.
* If `immediate` is passed, trigger the resolve callback on the leading edge, instead of the trailing.
* Previous promise will be rejected if a new one is initialized.
*
* `.cancel()` can be called manually to cancel the scheduled *trailing* invocation and reject the promise.
*
* ### **Note**: The debounced executor function **must** be persistent.
*
* @example
* const debounced = getDebouncedPromiseExecutor(500)
* async function calledOften() { return new Promise(debounced).then(() => console.log('once')) }
*
* @example
* const debounced = getDebouncedPromiseExecutor(500)
* const debouncedPromise = async <T>(value?: T): Promise<T | undefined> => new Promise(debounced).then(() => value)
*
* await somePromise.then(debouncedPromise).then(v => debouncedPromise(v))
* await debouncedPromise(someValue)
*
* @param wait - [milliseconds]
* @param immediate - *(optional)* Control whether the callback should be triggered on the leading or trailing edge of the wait interval.
*/
function getDebouncedPromiseExecutor(wait: number, immediate?: boolean) {
let timeout: NodeJS.Timeout | undefined;
let prevReject: PromiseParams[1] | undefined;
function debounced(...[resolve, reject]: PromiseParams): void {
prevReject?.('Debounced promise replaced.');
const later = () => {
timeout = undefined;
if (!immediate) resolve?.();
};
const callNow = immediate && !timeout;
timeout && clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) resolve?.();
else prevReject = reject;
}
debounced.cancel = () => {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
prevReject?.('Debounced promise cancelled.');
};
return debounced;
}
type PromiseParams = [resolve: (value?: any | PromiseLike<any>) => void, reject: (reason?: any) => void];
type RecursivelyReplaceNullWithUndefined<T> = T extends null
? undefined // Note: Add interfaces here of all GraphQL scalars that will be transformed into an object
: T extends Date
? T
: {
[K in keyof T]: T[K] extends (infer U)[]
? RecursivelyReplaceNullWithUndefined<U>[]
: RecursivelyReplaceNullWithUndefined<T[K]>;
};
/**
* Recursively replaces all nulls with undefineds.
* Skips object classes (that have a `.__proto__.constructor`).
*
* Unfortunately, until // https://github.com/apollographql/apollo-client/issues/2412#issuecomment-755449680
* gets solved at some point,
* this is the only workaround to prevent `null`s going into the codebase,
* if it's connected to a Apollo server/client.
*/
export function replaceNullsWithUndefineds<T>(
obj: T
): RecursivelyReplaceNullWithUndefined<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newObj: any = {};
Object.keys(obj).forEach((k) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const v: any = (obj as any)[k];
newObj[k as keyof T] =
v === null
? undefined
: // eslint-disable-next-line no-proto
v && typeof v === "object" && v.__proto__.constructor === Object
? replaceNullsWithUndefineds(v)
: v;
});
return newObj;
}
import { sleepResolve } from './sleep'
/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-poll-ts)
*
* @description Returns a promise that keeps calling a given function in set intervals
* until it returns a value other than `undefined` for the first time. If `timeout` is provided
* and the promise has not resolved yet, it will stop polling and get rejected instead.
*
* `.cancel()` can be called manually to reject the promise and stop polling.
*
* @param interval milliseconds
* @param timeout milliseconds
*
*
* @example
* // In the following example, the `resolver` function is called every 2 seconds
* // for up to 10 seconds, until it returns a value other than `undefined`.
* // It can be cancelled manually before that.
*
* let rVal = undefined
*
* function resolver() {
* console.log("-")
* return rVal
* }
*
* const p = poll(resolver, 2000, 10000)
*
* const p2 = p.then(v => console.log("resolved:", v), e => console.log("rejected:", e))
*
* // p.cancel() // manually cancel the polling
* // rVal = 42 // simulate a response
*/
export default function poll<R>(resolver: AnyFunction<R | undefined>, interval: number, timeout?: number): Promise<R> & Cancellable {
const promise: Promise<R> & Cancellable = new Promise<R>(async function (this: Cancellable, resolve, reject) {
let cancelled = false
const startTime = Date.now()
const endTime = timeout ? startTime + timeout : Infinity
setTimeout(() =>
promise.cancel = () => {
expire()
cancelled = true
reject('Poll: Cancelled by the user.')
}
)
function expire() { ref && clearTimeout(ref) }
// We use setTimeout for the rejection, because mere looping could be not granular enough.
const ref = timeout && setTimeout(reject, timeout, 'Poll: Condition was not met in time.')
let value: R | undefined
while (value = resolver(), value === undefined) {
if (Date.now() >= endTime) return // Time is out, promise should be already rejected by now. Let's bail out.
if (cancelled) return
await sleepResolve(interval)
}
// Condition was met, clean up and resolve
expire()
resolve(value)
}) as Promise<R> & Cancellable
return promise
}
interface Cancellable { cancel(): void }
// TS
export const sleepResolve = <T = void>(msec: number, retVal?: T) => new Promise<T>((resolve) => setTimeout(resolve, msec, retVal))
export const sleepReject = <T = void>(msec: number, retVal?: T) => new Promise<T>((_, reject) => setTimeout(reject, msec, retVal))
// JS
const sleepResolve = (msec, retVal) => new Promise((resolve) => setTimeout(resolve, msec, retVal))
const sleepReject = (msec, retVal) => new Promise((_, reject) => setTimeout(reject, msec, retVal))
/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-slugify-ts)
*
* @description Extracts text content from any React node and returns an URL-friendly representation of any text. Useful for page anchors.
*
* example https://codesandbox.io/s/react-getnodetext-h21ss?file=/src/App.js
*/
const slugifyNode = (node: JSX.Element) => slugify(getNodeText(node))
const slugify = (label: string = '') => label.toLowerCase().replace(/[’']/g, '').replace(/[^a-z0-9_]+/g, '-').replace(/^-+|-+$/g, '')
const getNodeText = (node: JSX.Element): string => {
if (['string', 'number'].includes(typeof node)) return node
if (node instanceof Array) return node.map(getNodeText).join('')
if (typeof node === 'object' && node) return getNodeText(node.props.children)
}
/**
* ## Smash that button!
*
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-smash-ts)
*
* @description Returns a function, that will call the provided callback only after being invoked a specified number of times (`threshold`),
* optionally within a `cooldown` period, that resets the counter if the function is not called again.
*
* *note:* This function can be combined with `debounce`,
* e.g. `debounce(smash(myFunction, ...), 100)` - to create a function that is triggered after a certain number of calls, but only if the calls are spaced out by a certain amount of time. *(Can't smash too fast!)*
*
* @param threshold Number of times the function must be called before the callback is triggered.
* @param cooldown `[milliseconds]` *(optional)* - If provided, the counter will spontaneously reset after set amount of time since last call; or first call with `immediate = true`.
* @param immediate *(optional)* - Start the cooldown timeout immediately after the first call only, instead of after every last call.
*/
export default function smash<Callback extends AnyFunction>(cb: Callback, threshold: number, cooldown?: number, immediate?: boolean) {
let timeout: NodeJS.Timeout | null
let counter = threshold
function smashable(this: any, ...args: Parameters<Callback>): void {
counter--
if (cooldown && timeout && !immediate) {
clearTimeout(timeout)
timeout = null
}
if (cooldown && !timeout) timeout = setTimeout(smashable.reset, cooldown)
if (counter <= 0) {
cb.apply(this, args)
smashable.reset()
}
}
smashable.reset = () => {
counter = threshold
timeout && clearTimeout(timeout)
timeout = null
}
return smashable
}
/* TypsScript Playground gives some useful error hints for this code
https://www.typescriptlang.org/play?#code/EQVwzgpgBGAuBOBLAxrYUD0GqF4NwEjtQICeUyA9gLYUQB2siNA5oQBaJgBQAbgIbyk8ANoIjQAvFADkfeAEkaAMRqTM2fKRY8m0WGzBQtAEyhkARpHhdoAWlbQAZiBqpEZGlBo9qhMlFPRkIRFDAC4oR3cAHwN4fmj7GgBGKHiaACYU8JoAZkyZeSUOKCgOcho4E1MAKygJAG8ijy8IMMkzKskAGkasKABRHmQWcKcXNyhECgAHQRREWEESQwh7Bgh9ef0AA112LagAd3nhniheQRBoZanaQwZmcZYyA4nYCf0AJScAOkaIgAoAJRQOqkNxgMgib6CMiMf6SCJdVjsYEAX26xV6AEFYs8Rs56OCoAARADyADlJAAVKDLVY0HQsCCIfjPdw7PRbToTcqwCA8YzAXQQEiBKawEDwRnQDl7cLwSh2FkklYMe4wcg3YC-YoyMJA2oAPjB5UhEGhsPhMiRuzAgIxWUSYQiY3cCQNoLKEKhMLhCKSNr0aIdCTSztGhLdNA9Ju95t98NDgZRUFR31MDEM-ycdLWhntfxy4YJrijMa9ZotfoS2WTdtT6cz-1t9tUhyZUdy7AMYDAiEYDOMhkl6q27X2yClPEj3NlYH2UvsEHg+lgvk2JgO7jAmugRg8ZDeY+qWx1bYSABZi66suXwZWE-6L3W0Y2aFn2q3eh8VsvaMgID6XF4DCdp3gPN5c0HRp8kUaNgQaYpMWwXh+FtNt+SGfEb3+GhDxiBUXhdSN6yg9YoDnfYeA2exXkOaiDDOPhEB4UwoUaZCkIkMCkN6WRaPQ6hdDIYxu3gJx5UVMDjyqb5YKUIEuQo20tgAAu7ai+wHCBjGFJiLmgMhaJkrkOLbYoJAAdUzPFekQWiGDgflRLeMSJKENwZXk+ClMoqBF2XVdfC2az32efZ-jIfgthzVVB32BgYAQFA3goEToH+UBICSpBUGAQFATPYozIrH1LWkWICmUbkWzbPyApXHw7EqKoIFQVhp38pxVyZbCZwmWjAmEHSexauTKrgg0-IkEyipKJDY1cmgGXgHF+AkA0xGNUr43K7yoGNBgVrWl86pU-zf0atdmuQGFIAqHcyBuDrILi8jKLmsziNLHkVoUJwPTMzjUORfQrJsl4oqgWL6RGuzplmZB5mUzkgdjB89ompQDt+5d-uq0HgV6PTbrIe6jxU9T9HsBUKAoo68acLY0a9V6ICmNbaigTbtvvMq-X2w7lsZ9xjWWdmTpqoM2xJu71gpzkqckumtgZ+B8eZhbinFjnYiBMzUTMsy1bW-WFrV-GzdTbpDdKcE3naDJ6kaTxqFaR3JA4W27dNfn-i2AAdAAPRJTAAbWyAA2CgxFjuOBGG4xxjA2xg9DiO0gAbgvAAGNLqm+AASOohpECBUSBNPw7zrZAQ4dow9L0QAF19Z9uMq39quI+juP46b4wPIZKBU5D8Psiz3OKGLpvy8rsew5ruuQYSLmG9n5uOHdOv24xuFA4XqOY77iQB78MgcxHqBu5z7O8-+buJ-z2SZ6CUQ0wzd9IuqQEb7vihf6HzSBQee6cl7cD4OfS+3FqiNzfhAZub4Pw-w4L0EGpgL7vi5gkJB38qhE2wOhbsekwDNA4BgnMbcdqdwPunI+J9T5vyTu4LQh4mSsmqG1N4o86GTzzo7IudR-SSAroAsBFBa712qGkb4q8YFVDgcNBBqDsACLkVkGWehwIkLIWo+CHBd5+1oePXuJ8E7BCgVg66rDhQcNau1Hh1d-4PyAc-QRs8P5NkdmIpxU8fEZxAf48B6DMGGCdi1RRZdEGf2QVUNIdc0GQIoe+cJOCYl4PiZo7sxDeqkOoOQ0JaQgRAA
*/
"use strict" // 👈 try commenting this
var callee = 'arrInFn' // 👈 change this and observe - the function name to be called: fun | arr | fn1 | fn2 | fn3 | arrInFn
const obj = {
name: 'obj',
// Each function implicitly defines its `this` with a value depending on how it is Run.
fun() { console.log('fun', this) },
// Arrow functions DON'T define their own `this`, instead "they capture the `this` from their Defining scope".
arr: () => console.log('arr', this),
fn1: function fn() { console.log('fn1', this) },
fn2: function fn() { console.log('fn2', this) }.bind(undefined),
fn3: function fn() { console.log('fn3', this) }.bind(this), // when fn3 is assigned during `obj` creation, `this` refers to its own scope and not `obj`.
// fn4: function fn() { console.log('fn4', this) }.bind(obj), // ReferenceError: obj is not defined
arrInFn() {
// var this // each function (not arrow functions) defines `this` as if it was a variable.
// = obj // If this method is run from obj `obj.arrInFn()`, `this` is assigned the value of `obj`,
// = Window // if instead it is run alone `arrInFn()`, `this` refers to `Window` (or `undefined` in strict mode ("use strict")).
console.log('arrInFn', this) // `this` refers to the object that runs the function, if called as obj.arrInFn() `this` = `obj`.
const innerArr = () => console.log('arrInFn > innerArr', this) // `this` refers to the closest scope that defines `this`.
function innerFun() {
// var this = Window or undefined // implicit `this`
console.log('arrInFn > innerFun', this) // the closest `this` is from `innerFun`
const deepArr = () => console.log('arrInFn > innerFun > deepArr', this) // the closest `this` is from `innerFun`
deepArr()
}
innerArr()
innerFun()
},
}
const obj2 = {
name: 'obj2'
}
console.log(`\x1b[36m====== called on obj - \x1b[32;40mobj.${callee}()\x1b[0m`)
obj[callee]()
console.log(`\x1b[36m====== called alone - \x1b[32;40m${callee}()\x1b[0m`)
var fn = obj[callee]
fn()
console.log(`\x1b[36m====== called bound - \x1b[0;40m(\x1b[32mobj.${callee}.bind(obj)\x1b[0;40m)\x1b[32m()\x1b[0m`)
var bound = obj[callee].bind(obj)
// var bound = fn.bind(obj) // this is the same
bound()
console.log(`\x1b[36m====== called on another object - \x1b[32;40mobj2.${'fn'}()\x1b[0m`)
obj2.fn = obj[callee]
// obj2.fn = fn // this is the same
obj2.fn()
console.log(`\x1b[36m====== called bound to another object - \x1b[0;40m(\x1b[32mobj.${callee}.bind(obj2)\x1b[0;40m)\x1b[32m()\x1b[0m`)
var bound2 = obj[callee].bind(obj2)
// var bound2 = fn.bind(obj2) // this is the same
bound2()
/**
* @author Qwerty <[email protected]>
* @link [gist](https://gist.github.com/ackvf/68f992660a5eda645c4671d3599b2acf#file-withproxyfallback-ts)
*
* @description
* Higher order function to wrap an object and provide a default value for unknown properties.
*
* @param fallback The property name or value to use for unknown properties
* @param obj The object to wrap
* @returns Proxy around the original Object with default value fallback
*
* @example
* const safeProp = withProxyFallback("a", { a: 1 })
* const safeValue = withProxyFallback(42, { a: 1 })
* safeProp.b // 1 (falls back to default property "a")
* safeValue.b // 42 (falls back to default value 42)
*/
export function withProxyFallback<T extends AnyObject, K extends keyof T>(
fallback: K | T[K],
obj: T,
): T {
return new Proxy(obj, {
get(target, prop: string) {
if (prop in target) {
return target[prop as keyof T]
}
if (typeof fallback === 'string' && fallback in target) {
return target[fallback as keyof T]
}
return fallback
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment