Skip to content

Instantly share code, notes, and snippets.

@norbornen
Last active June 28, 2024 17:42
Show Gist options
  • Save norbornen/e358a0595cef7d8d9d55e31fefd72859 to your computer and use it in GitHub Desktop.
Save norbornen/e358a0595cef7d8d9d55e31fefd72859 to your computer and use it in GitHub Desktop.
typescript-promisify-all-entity-methods
import { promisify } from 'node:util';
import type { ArraySlice, LastArrayElement } from 'type-fest';
type AnyoneFunction = (...args: any) => any;
type PickCallbackReturnType<F extends AnyoneFunction> =
LastArrayElement<Parameters<F>> extends AnyoneFunction
? Promise<LastArrayElement<Parameters<LastArrayElement<Parameters<F>>>>>
: never;
type PromisableKeysOf<BaseType extends object> = Exclude<
{
[Key in keyof BaseType]: Key extends string
? BaseType[Key] extends AnyoneFunction
? Key
: never
: never;
}[keyof BaseType],
undefined
>;
export type PromisifiedObject<
BaseType extends object,
BaseTypeKey extends PromisableKeysOf<BaseType> = PromisableKeysOf<BaseType>,
> = BaseType & {
[Key in BaseTypeKey &
string as `${Key}Async`]: BaseType[Key] extends AnyoneFunction
? (
...args: ArraySlice<Parameters<BaseType[Key]>, 0, -1>
) => PickCallbackReturnType<BaseType[Key]>
: never;
};
const METHODS_BLACKLIST = Object.getOwnPropertyNames(Object.prototype).reduce(
(acc, methodName) => acc.add(methodName),
new Set<string>(),
);
function getAllMethods<
BaseType extends Record<string, any>,
R = PromisableKeysOf<BaseType>,
>(obj: BaseType): Set<R> {
const methodNames = new Set<string>();
let currentObj = obj;
do {
Object.getOwnPropertyNames(currentObj).map((item) => methodNames.add(item));
} while ((currentObj = Object.getPrototypeOf(currentObj)));
return new Set(
[...methodNames].filter(
(item) => typeof obj[item] === 'function' && !METHODS_BLACKLIST.has(item),
) as R[],
);
}
/**
* @example
* class A {
* method(payload, callback: () => void) {
* callback(null, { ok: 1 });
* }
* }
* const entity = promisifyAll(new A());
* await entity.methodAsync({ hello: 'world' });
* entity.method({ hello: 'world' }, () => {});
*/
export function promisifyAll<
BaseType extends object,
BaseTypeKey extends PromisableKeysOf<BaseType>,
R = PromisifiedObject<BaseType, BaseTypeKey>,
>(mutableObject: BaseType, ...inputMethodNames: BaseTypeKey[]): R {
const allMethods = getAllMethods(mutableObject);
const keys =
!inputMethodNames || inputMethodNames.length === 0
? ([...allMethods] as BaseTypeKey[])
: inputMethodNames.filter((key) => allMethods.has(key));
const result = mutableObject as unknown as R;
keys.forEach((key) => {
const asyncAlias = `${key}Async` as keyof R;
if (!result[asyncAlias]) {
const fn = mutableObject[key] as BaseType[BaseTypeKey] & AnyoneFunction;
result[asyncAlias] = promisify(fn).bind(mutableObject);
}
});
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment