Skip to content

Commit

Permalink
Add getKnownPropertyNames (#111)
Browse files Browse the repository at this point in the history
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 <maarten@zuidhoorn.com>
  • Loading branch information
mcmire and Mrtenz authored Jul 13, 2023
1 parent ab071ea commit a1e5d90
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 1 deletion.
20 changes: 19 additions & 1 deletion src/misc.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expectAssignable, expectNotAssignable, expectType } from 'tsd';

import { isObject, hasProperty, RuntimeObject } from './misc';
import {
isObject,
hasProperty,
getKnownPropertyNames,
RuntimeObject,
} from './misc';

//=============================================================================
// isObject
Expand Down Expand Up @@ -99,6 +104,19 @@ if (hasProperty(hasPropertyTypeExample, 'a')) {
expectType<number | undefined>(hasPropertyTypeExample.a);
}

//=============================================================================
// getKnownPropertyNames
//=============================================================================

enum GetKnownPropertyNamesEnumExample {
Foo = 'bar',
Baz = 'qux',
}

expectType<('Foo' | 'Baz')[]>(
getKnownPropertyNames(GetKnownPropertyNamesEnumExample),
);

//=============================================================================
// RuntimeObject
//=============================================================================
Expand Down
16 changes: 16 additions & 0 deletions src/misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
isNullOrUndefined,
isObject,
hasProperty,
getKnownPropertyNames,
RuntimeObject,
isPlainObject,
calculateNumberSize,
Expand Down Expand Up @@ -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 = {
Expand Down
17 changes: 17 additions & 0 deletions src/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key extends PropertyKey>(
object: Partial<Record<Key, any>>,
): Key[] {
return Object.getOwnPropertyNames(object) as Key[];
}

export type PlainObject = Record<number | string | symbol, unknown>;

/**
Expand Down

0 comments on commit a1e5d90

Please sign in to comment.