Skip to content

Commit a1e5d90

Browse files
mcmireMrtenz
andauthored
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 <[email protected]>
1 parent ab071ea commit a1e5d90

File tree

3 files changed

+52
-1
lines changed

3 files changed

+52
-1
lines changed

src/misc.test-d.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { expectAssignable, expectNotAssignable, expectType } from 'tsd';
22

3-
import { isObject, hasProperty, RuntimeObject } from './misc';
3+
import {
4+
isObject,
5+
hasProperty,
6+
getKnownPropertyNames,
7+
RuntimeObject,
8+
} from './misc';
49

510
//=============================================================================
611
// isObject
@@ -99,6 +104,19 @@ if (hasProperty(hasPropertyTypeExample, 'a')) {
99104
expectType<number | undefined>(hasPropertyTypeExample.a);
100105
}
101106

107+
//=============================================================================
108+
// getKnownPropertyNames
109+
//=============================================================================
110+
111+
enum GetKnownPropertyNamesEnumExample {
112+
Foo = 'bar',
113+
Baz = 'qux',
114+
}
115+
116+
expectType<('Foo' | 'Baz')[]>(
117+
getKnownPropertyNames(GetKnownPropertyNamesEnumExample),
118+
);
119+
102120
//=============================================================================
103121
// RuntimeObject
104122
//=============================================================================

src/misc.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isNullOrUndefined,
44
isObject,
55
hasProperty,
6+
getKnownPropertyNames,
67
RuntimeObject,
78
isPlainObject,
89
calculateNumberSize,
@@ -116,6 +117,21 @@ describe('miscellaneous', () => {
116117
});
117118
});
118119

120+
describe('getKnownPropertyNames', () => {
121+
it('returns the own property names of the object', () => {
122+
const object = { foo: 'bar', baz: 'qux' };
123+
124+
expect(getKnownPropertyNames(object)).toStrictEqual(['foo', 'baz']);
125+
});
126+
127+
it('does not return inherited properties', () => {
128+
const superObject = { foo: 'bar' };
129+
const object = Object.create(superObject);
130+
131+
expect(getKnownPropertyNames(object)).toStrictEqual([]);
132+
});
133+
});
134+
119135
describe('isPlainObject', () => {
120136
it('should return true for a plain object', () => {
121137
const somePlainObject = {

src/misc.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ export const hasProperty = <
9999
Property extends keyof ObjectToCheck ? ObjectToCheck[Property] : unknown
100100
> => Object.hasOwnProperty.call(objectToCheck, name);
101101

102+
/**
103+
* `Object.getOwnPropertyNames()` is intentionally generic: it returns the
104+
* immediate property names of an object, but it cannot make guarantees about
105+
* the contents of that object, so the type of the property names is merely
106+
* `string[]`. While this is technically accurate, it is also unnecessary if we
107+
* have an object with a type that we own (such as an enum).
108+
*
109+
* @param object - The plain object.
110+
* @returns The own property names of the object which are assigned a type
111+
* derived from the object itself.
112+
*/
113+
export function getKnownPropertyNames<Key extends PropertyKey>(
114+
object: Partial<Record<Key, any>>,
115+
): Key[] {
116+
return Object.getOwnPropertyNames(object) as Key[];
117+
}
118+
102119
export type PlainObject = Record<number | string | symbol, unknown>;
103120

104121
/**

0 commit comments

Comments
 (0)