Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bindAllMethods util function #1476

Merged
merged 2 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions packages/utils/src/ClassUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable max-classes-per-file */
import { bindAllMethods, getAllMethodNames } from './ClassUtils';

class Aaa {
nameA = 'Aaa';

getAaa() {
return this.nameA;
}
}

class Bbb extends Aaa {
nameB = 'Bbb';

getBbb() {
return this.nameB;
}
}

class Ccc extends Bbb {
nameC = 'Ccc';

getCcc() {
return this.nameC;
}

getCcc2 = () => this.nameC;
}

beforeEach(() => {
jest.clearAllMocks();
expect.hasAssertions();
});

describe('getAllMethodNames', () => {
it.each([true, false])(
'should return all method names: %s',
traversePrototypeChain => {
const instance = new Ccc();

const methodNames = getAllMethodNames(
instance,
traversePrototypeChain
).sort();

if (traversePrototypeChain) {
expect(methodNames).toEqual(['getAaa', 'getBbb', 'getCcc', 'getCcc2']);
} else {
expect(methodNames).toEqual(['getCcc', 'getCcc2']);
}
}
);
});

describe('bindAllMethods', () => {
it.each([true, false, undefined])(
'should bind all methods: %s',
traversePrototypeChain => {
const instance = new Ccc();

bindAllMethods(instance, traversePrototypeChain);

const { getAaa, getBbb, getCcc, getCcc2 } = instance;

if (traversePrototypeChain === true) {
expect(getAaa()).toEqual('Aaa');
expect(getBbb()).toEqual('Bbb');
} else {
expect(() => getAaa()).toThrow(
"Cannot read properties of undefined (reading 'nameA')"
);
expect(() => getBbb()).toThrow(
"Cannot read properties of undefined (reading 'nameB')"
);
}

expect(getCcc()).toEqual('Ccc');
expect(getCcc2()).toEqual('Ccc');
}
);
});
65 changes: 65 additions & 0 deletions packages/utils/src/ClassUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export type MethodName<T> = {
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
}[keyof T];

/**
* Bind all methods on the instance + its prototype to the instance. If
* `traversePrototypeChain` is true, the prototype chain will be traversed until
* Object.prototype is reached, and any additional methods found will be included.
* @param instance The instance to bind methods to
* @param traversePrototypeChain Whether to traverse the prototype chain or not
*/
export function bindAllMethods(
instance: object,
traversePrototypeChain = false
): void {
const methodNames = getAllMethodNames(instance, traversePrototypeChain);

methodNames.forEach(methodName => {
// eslint-disable-next-line no-param-reassign
(instance as Record<string, unknown>)[methodName] = (
instance[methodName] as (...args: unknown[]) => unknown
).bind(instance);
});
}

/**
* Get all class method names. This will return names of all methods defined on
* the instance + its prototype. If `traversePrototypeChain` is true, the prototype
* chain will be traversed until Object.prototype is reached, and any additional
* methods found will be included.
* @param instance Instance to get method names from
* @param traversePrototypeChain Whether to traverse the prototype chain or not
*/
export function getAllMethodNames<T>(
instance: T,
traversePrototypeChain: boolean
): MethodName<T>[] {
const methodNames = new Set<MethodName<T>>();

let current = instance;

// Get method names for instance + prototype. Optionally traverse prototype
// chain until Object.prototype is reached.
let level = 0;
while (
current != null &&
current !== Object.prototype &&
(level <= 1 || traversePrototypeChain)
) {
// eslint-disable-next-line no-restricted-syntax
for (const name of Object.getOwnPropertyNames(current)) {
if (
name !== 'constructor' &&
typeof current[name as keyof typeof current] === 'function'
) {
methodNames.add(name as MethodName<T>);
}
}

current = Object.getPrototypeOf(current);
level += 1;
}

return [...methodNames.keys()];
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './DataUtils';
export { default as CanceledPromiseError } from './CanceledPromiseError';
export * from './ClassUtils';
export { default as ColorUtils } from './ColorUtils';
export * from './ClipboardUtils';
export { default as DbNameValidator } from './DbNameValidator';
Expand Down