From a1e5d9037b862b16f4f5b7bd26cfdd7de9d12713 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 13 Jul 2023 10:54:20 -0600 Subject: [PATCH] Add getKnownPropertyNames (#111) This function has been copied from various projects, where it is common to transform an enum into another data structure. For instance: ``` enum InfuraNetworkType { mainnet = 'mainnet', goerli = 'goerli', sepolia = 'sepolia', } const infuraNetworkClientConfigurations = Object.keys(InfuraNetworkType).map((network) => { const networkClientId = buildInfuraNetworkClientId(network); const networkClientConfiguration = { type: NetworkClientType.Infura, network, infuraProjectId: this.#infuraProjectId, }; return [networkClientId, networkClientConfiguration]; }); ``` As the above example, one could use `Object.keys()` or even `Object.getOwnPropertyNames()` to obtain said properties. A problem occurs, however, if the type of the properties of the resulting object needs to match the type of the properties in the enum, that means the variable inside the loop needs to be of that type, too. Both `Object.keys()` and `Object.getOwnPropertyNames()` are intentionally generic: they returns the property names of an object, but neither can make guarantees about the contents of that object, so the type of the property names is merely `string[]`. While this is technically accurate, we don't have to be so cautious in these situations, because we own the object in question and therefore know exactly which properties it has. This commit adds a `getKnownPropertyNames` function which is like `Object.getOwnPropertyNames()` except that the resulting array of property names will be typed using the types of the properties of the given object. In the above example that would mean that `network` would have a type of `InfuraNetworkType` and not `string`. Co-authored-by: Maarten Zuidhoorn --- src/misc.test-d.ts | 20 +++++++++++++++++++- src/misc.test.ts | 16 ++++++++++++++++ src/misc.ts | 17 +++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/misc.test-d.ts b/src/misc.test-d.ts index cb0ed7735..4abec5ab1 100644 --- a/src/misc.test-d.ts +++ b/src/misc.test-d.ts @@ -1,6 +1,11 @@ import { expectAssignable, expectNotAssignable, expectType } from 'tsd'; -import { isObject, hasProperty, RuntimeObject } from './misc'; +import { + isObject, + hasProperty, + getKnownPropertyNames, + RuntimeObject, +} from './misc'; //============================================================================= // isObject @@ -99,6 +104,19 @@ if (hasProperty(hasPropertyTypeExample, 'a')) { expectType(hasPropertyTypeExample.a); } +//============================================================================= +// getKnownPropertyNames +//============================================================================= + +enum GetKnownPropertyNamesEnumExample { + Foo = 'bar', + Baz = 'qux', +} + +expectType<('Foo' | 'Baz')[]>( + getKnownPropertyNames(GetKnownPropertyNamesEnumExample), +); + //============================================================================= // RuntimeObject //============================================================================= diff --git a/src/misc.test.ts b/src/misc.test.ts index e6ce17066..1e5007280 100644 --- a/src/misc.test.ts +++ b/src/misc.test.ts @@ -3,6 +3,7 @@ import { isNullOrUndefined, isObject, hasProperty, + getKnownPropertyNames, RuntimeObject, isPlainObject, calculateNumberSize, @@ -116,6 +117,21 @@ describe('miscellaneous', () => { }); }); + describe('getKnownPropertyNames', () => { + it('returns the own property names of the object', () => { + const object = { foo: 'bar', baz: 'qux' }; + + expect(getKnownPropertyNames(object)).toStrictEqual(['foo', 'baz']); + }); + + it('does not return inherited properties', () => { + const superObject = { foo: 'bar' }; + const object = Object.create(superObject); + + expect(getKnownPropertyNames(object)).toStrictEqual([]); + }); + }); + describe('isPlainObject', () => { it('should return true for a plain object', () => { const somePlainObject = { diff --git a/src/misc.ts b/src/misc.ts index 45b7401c7..700d46760 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -99,6 +99,23 @@ export const hasProperty = < Property extends keyof ObjectToCheck ? ObjectToCheck[Property] : unknown > => Object.hasOwnProperty.call(objectToCheck, name); +/** + * `Object.getOwnPropertyNames()` is intentionally generic: it returns the + * immediate property names of an object, but it cannot make guarantees about + * the contents of that object, so the type of the property names is merely + * `string[]`. While this is technically accurate, it is also unnecessary if we + * have an object with a type that we own (such as an enum). + * + * @param object - The plain object. + * @returns The own property names of the object which are assigned a type + * derived from the object itself. + */ +export function getKnownPropertyNames( + object: Partial>, +): Key[] { + return Object.getOwnPropertyNames(object) as Key[]; +} + export type PlainObject = Record; /**