Skip to content

Instantly share code, notes, and snippets.

@sirlancelot
Created November 27, 2024 17:37
Show Gist options
  • Save sirlancelot/8310b07fdc01136945e622a9bef6a360 to your computer and use it in GitHub Desktop.
Save sirlancelot/8310b07fdc01136945e622a9bef6a360 to your computer and use it in GitHub Desktop.
A generic way to overload a return value for supporting multiple, optional values.
/**
* Use this to return a non-primitive value which can be assigned to a single
* variable or destructured to assign multiple variables. It is strongly typed
* for ease of use.
*
* Usage:
*
* ```ts
* function somethingReturningMultipleValues() {
* const primaryResult = { id: 1, hello: "world" }
*
* return withOptionalValues(primaryResult, ["def", "ghi"] as const)
* }
* ```
*
* If your additional values are expensive to compute, you can use a factory
* function to generate them:
*
* ```ts
* function somethingReturningMultipleValues() {
* const primaryResult = { id: 1, hello: "world" }
*
* return withOptionalValues(primaryResult, () => ["def", "ghi"] as const)
* }
* ```
*
* This was inspired by Vue.js `defineModel()` function, which supports this
* same pattern in a more specific way: https://vuejs.org/guide/components/v-model.html
*/
/**
* A value with a `primary` value, which can also be destructured to assign
* optional values.
*/
export type WithOptionalValues<T extends object, U extends [...any]> = T &
[primary: T, ...more: U, ...bad: unknown[]]
/**
* Return a value with optional additional values accessible via destructuring.
*
* Overload 1: Factory function of additional values. For use when the
* additional values require extra computation.
*/
export function withOptionalValues<T extends object, U extends [...any]>(
primary: T,
factory: () => readonly [...U]
): WithOptionalValues<T, U>
/**
* Return a value with optional additional values accessible via destructuring.
*
* Overload 2: Static Array of additional values. For use when the values are
* cheap to compute.
*/
export function withOptionalValues<T extends object, U extends [...any]>(
primary: T,
more: readonly [...U]
): WithOptionalValues<T, U>
/** Implementation */
export function withOptionalValues<T extends object, U extends [...any]>(
primary: T,
maybeFactory: U | (() => U)
): WithOptionalValues<T, U> {
// @ts-expect-error
primary[Symbol.iterator] = function* () {
yield primary
yield* typeof maybeFactory === "function"
? maybeFactory()
: maybeFactory
}
return primary as any
}
@sirlancelot
Copy link
Author

Scenario, a memoization function which can optionally have its memory cleared. The return type function signature mirrors the passed in function exactly, but a consumer may wish to opt-in to enhanced behavior.

export function useCached<
	Key = string,
	Args extends any[] = never[],
	Ret = unknown
>(
	fn: (key: Key, ...args: Args) => Ret
): WithOptionalValues<
	(key: Key, ...Args: Args) => Ret,
	[clearAll: () => void, deleteOne: (key: Key) => boolean]
> {
	const cache = new Map<Key, Ret>()

	return withOptionalValues(
		function cachedFn(key: Key, ...args: Args) {
			if (cache.has(key)) return cache.get(key)!
			const value = fn(key, ...args)
			cache.set(key, value)
			return value
		},
		[() => cache.clear(), (key: Key) => cache.delete(key)]
	)
}

// =============================================================================

// Basic usage:
const parseData = useCached((key: string, node: HTMLElement) => {
	return JSON.parse(node.dataset[key] ?? "null")
})

// Opt-in to advanced usage:
const [parseData, clearParsedCache, deleteParsedOne] = useCached(
	(key: string, node: HTMLElement) => {
		return JSON.parse(node.dataset[key] ?? "null")
	}
)

@sirlancelot
Copy link
Author

If you try to destructure beyond the last optional value, you will get unknown:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment