Skip to content

Commit 85df0af

Browse files
Merge pull request Expensify#696 from blazejkustra/fix/update-typesafety
Refactor OnyxUpdate type to bring back type safety
2 parents bc5e9a2 + 0443e3b commit 85df0af

18 files changed

Lines changed: 207 additions & 74 deletions

.eslintignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
*.d.ts
22
dist
33
node_modules
4-
*.config.js
4+
*.config.js
5+
# tests/types catalog is not type checked with the rest of the project, so we need to ignore it in eslint
6+
tests/types/**/*.ts

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ jobs:
3030
- run: npm run test
3131
env:
3232
CI: true
33+
34+
- run: npm run test:types

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = {
44
transform: {
55
'\\.[jt]sx?$': 'babel-jest',
66
},
7-
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/tests/unit/mocks/', '<rootDir>/tests/e2e/'],
7+
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/tests/unit/mocks/', '<rootDir>/tests/e2e/', '<rootDir>/tests/types/'],
88
testMatch: ['**/tests/unit/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
99
globals: {
1010
__DEV__: true,

lib/Onyx.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import Storage from './storage';
44
import utils from './utils';
55
import DevTools, {initDevTools} from './DevTools';
66
import type {
7-
Collection,
8-
CollectionKey,
97
CollectionKeyBase,
108
ConnectOptions,
119
InitOptions,
@@ -15,6 +13,7 @@ import type {
1513
MixedOperationsQueue,
1614
OnyxKey,
1715
OnyxMergeCollectionInput,
16+
OnyxSetCollectionInput,
1817
OnyxMergeInput,
1918
OnyxMultiSetInput,
2019
OnyxSetInput,
@@ -374,7 +373,7 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
374373
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
375374
* @param collection Object collection keyed by individual collection member keys and values
376375
*/
377-
function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>): Promise<void> {
376+
function mergeCollection<TKey extends CollectionKeyBase>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey>): Promise<void> {
378377
return OnyxUtils.mergeCollectionWithPatches(collectionKey, collection, undefined, true);
379378
}
380379

@@ -545,7 +544,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
545544
[OnyxUtils.METHOD.SET]: enqueueSetOperation,
546545
[OnyxUtils.METHOD.MERGE]: enqueueMergeOperation,
547546
[OnyxUtils.METHOD.MERGE_COLLECTION]: () => {
548-
const collection = value as Collection<CollectionKey, unknown, unknown>;
547+
const collection = value as OnyxMergeCollectionInput<OnyxKey>;
549548
if (!OnyxUtils.isValidNonEmptyCollectionForMerge(collection)) {
550549
Logger.logInfo('mergeCollection enqueued within update() with invalid or empty value. Skipping this operation.');
551550
return;
@@ -558,7 +557,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
558557
collectionKeys.forEach((collectionKey) => enqueueMergeOperation(collectionKey, mergedCollection[collectionKey]));
559558
}
560559
},
561-
[OnyxUtils.METHOD.SET_COLLECTION]: (k, v) => promises.push(() => setCollection(k, v as Collection<CollectionKey, unknown, unknown>)),
560+
[OnyxUtils.METHOD.SET_COLLECTION]: (k, v) => promises.push(() => setCollection(k, v as OnyxSetCollectionInput<OnyxKey>)),
562561
[OnyxUtils.METHOD.MULTI_SET]: (k, v) => Object.entries(v as Partial<OnyxInputKeyValueMapping>).forEach(([entryKey, entryValue]) => enqueueSetOperation(entryKey, entryValue)),
563562
[OnyxUtils.METHOD.CLEAR]: () => {
564563
clearPromise = clear();
@@ -611,14 +610,14 @@ function update(data: OnyxUpdate[]): Promise<void> {
611610
promises.push(() =>
612611
OnyxUtils.mergeCollectionWithPatches(
613612
collectionKey,
614-
batchedCollectionUpdates.merge as Collection<CollectionKey, unknown, unknown>,
613+
batchedCollectionUpdates.merge as OnyxMergeCollectionInput<OnyxKey>,
615614
batchedCollectionUpdates.mergeReplaceNullPatches,
616615
true,
617616
),
618617
);
619618
}
620619
if (!utils.isEmptyObject(batchedCollectionUpdates.set)) {
621-
promises.push(() => OnyxUtils.partialSetCollection(collectionKey, batchedCollectionUpdates.set as Collection<CollectionKey, unknown, unknown>));
620+
promises.push(() => OnyxUtils.partialSetCollection(collectionKey, batchedCollectionUpdates.set as OnyxSetCollectionInput<OnyxKey>));
622621
}
623622
});
624623

@@ -655,7 +654,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
655654
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
656655
* @param collection Object collection keyed by individual collection member keys and values
657656
*/
658-
function setCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>): Promise<void> {
657+
function setCollection<TKey extends CollectionKeyBase>(collectionKey: TKey, collection: OnyxSetCollectionInput<TKey>): Promise<void> {
659658
let resultCollection: OnyxInputKeyValueMapping = collection;
660659
let resultCollectionKeys = Object.keys(resultCollection);
661660

lib/OnyxUtils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
OnyxUpdate,
3030
OnyxValue,
3131
Selector,
32+
OnyxSetCollectionInput,
3233
} from './types';
3334
import type {FastMergeOptions, FastMergeResult} from './utils';
3435
import utils from './utils';
@@ -1089,7 +1090,7 @@ function initializeWithDefaultKeyStates(): Promise<void> {
10891090
/**
10901091
* Validate the collection is not empty and has a correct type before applying mergeCollection()
10911092
*/
1092-
function isValidNonEmptyCollectionForMerge<TKey extends CollectionKeyBase, TMap>(collection: OnyxMergeCollectionInput<TKey, TMap>): boolean {
1093+
function isValidNonEmptyCollectionForMerge<TKey extends CollectionKeyBase>(collection: OnyxMergeCollectionInput<TKey>): boolean {
10931094
return typeof collection === 'object' && !Array.isArray(collection) && !utils.isEmptyObject(collection);
10941095
}
10951096

@@ -1295,9 +1296,9 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
12951296
* @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
12961297
* tuples that we'll use to replace the nested objects of that collection member record with something else.
12971298
*/
1298-
function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(
1299+
function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(
12991300
collectionKey: TKey,
1300-
collection: OnyxMergeCollectionInput<TKey, TMap>,
1301+
collection: OnyxMergeCollectionInput<TKey>,
13011302
mergeReplaceNullPatches?: MultiMergeReplaceNullPatches,
13021303
isProcessingCollectionUpdate = false,
13031304
): Promise<void> {
@@ -1415,7 +1416,7 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(
14151416
});
14161417

14171418
return Promise.all(promises)
1418-
.catch((error) => evictStorageAndRetry(error, mergeCollectionWithPatches, collectionKey, resultCollection))
1419+
.catch((error) => evictStorageAndRetry(error, mergeCollectionWithPatches, collectionKey, resultCollection as OnyxMergeCollectionInput<TKey>))
14191420
.then(() => {
14201421
sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection);
14211422
return promiseUpdate;
@@ -1431,7 +1432,7 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(
14311432
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
14321433
* @param collection Object collection keyed by individual collection member keys and values
14331434
*/
1434-
function partialSetCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>): Promise<void> {
1435+
function partialSetCollection<TKey extends CollectionKeyBase>(collectionKey: TKey, collection: OnyxSetCollectionInput<TKey>): Promise<void> {
14351436
let resultCollection: OnyxInputKeyValueMapping = collection;
14361437
let resultCollectionKeys = Object.keys(resultCollection);
14371438

lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
OnyxMultiSetInput,
1717
OnyxMergeInput,
1818
OnyxMergeCollectionInput,
19+
OnyxSetCollectionInput,
1920
} from './types';
2021
import type {FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions} from './useOnyx';
2122
import type {Connection} from './OnyxConnectionManager';
@@ -40,6 +41,7 @@ export type {
4041
OnyxMultiSetInput,
4142
OnyxMergeInput,
4243
OnyxMergeCollectionInput,
44+
OnyxSetCollectionInput,
4345
OnyxUpdate,
4446
OnyxValue,
4547
ResultMetadata,

lib/types.ts

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type {Merge} from 'type-fest';
2-
import type {BuiltIns} from 'type-fest/source/internal';
32
import type OnyxUtils from './OnyxUtils';
43
import type {OnyxMethod} from './OnyxUtils';
54
import type {FastMergeReplaceNullPatch} from './utils';
@@ -157,6 +156,10 @@ type OnyxValue<TKey extends OnyxKey> = string extends TKey ? unknown : TKey exte
157156
/** Utility type to extract `TOnyxValue` from `OnyxCollection<TOnyxValue>` */
158157
type ExtractOnyxCollectionValue<TOnyxCollection> = TOnyxCollection extends NonNullable<OnyxCollection<infer U>> ? U : never;
159158

159+
type Primitive = null | undefined | string | number | boolean | symbol | bigint;
160+
161+
type BuiltIns = Primitive | void | Date | RegExp;
162+
160163
type NonTransformableTypes =
161164
| BuiltIns
162165
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -205,13 +208,7 @@ type NullishObjectDeep<ObjectType extends object> = {
205208
* Also, the `TMap` type is inferred automatically in `mergeCollection()` method and represents
206209
* the object of collection keys/values specified in the second parameter of the method.
207210
*/
208-
type Collection<TKey extends CollectionKeyBase, TValue, TMap = never> = {
209-
[MapK in keyof TMap]: MapK extends `${TKey}${string}`
210-
? MapK extends `${TKey}`
211-
? never // forbids empty id
212-
: TValue
213-
: never;
214-
};
211+
type Collection<TKey extends CollectionKeyBase, TValue> = Record<`${TKey}${string}`, TValue> & {[P in TKey]?: never};
215212

216213
/** Represents the base options used in `Onyx.connect()` method. */
217214
// NOTE: Any changes to this type like adding or removing options must be accounted in OnyxConnectionManager's `generateConnectionID()` method!
@@ -322,48 +319,58 @@ type OnyxMergeInput<TKey extends OnyxKey> = OnyxInput<TKey>;
322319
/**
323320
* This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE"
324321
*/
325-
type OnyxMergeCollectionInput<TKey extends OnyxKey, TMap = object> = Collection<TKey, NonNullable<OnyxInput<TKey>>, TMap>;
322+
type OnyxMergeCollectionInput<TKey extends OnyxKey> = Collection<TKey, NonNullable<OnyxInput<TKey>>>;
326323

327-
type OnyxMethodMap = typeof OnyxUtils.METHOD;
324+
/**
325+
* This represents the value that can be passed to `Onyx.setCollection` and to `Onyx.update` with the method "SET_COLLECTION"
326+
*/
327+
type OnyxSetCollectionInput<TKey extends OnyxKey> = Collection<TKey, OnyxInput<TKey>>;
328328

329-
// Maps onyx methods to their corresponding value types
330-
type OnyxMethodValueMap = {
331-
[OnyxUtils.METHOD.SET]: {
332-
key: OnyxKey;
333-
value: OnyxSetInput<OnyxKey>;
334-
};
335-
[OnyxUtils.METHOD.MULTI_SET]: {
336-
key: OnyxKey;
337-
value: OnyxMultiSetInput;
338-
};
339-
[OnyxUtils.METHOD.MERGE]: {
340-
key: OnyxKey;
341-
value: OnyxMergeInput<OnyxKey>;
342-
};
343-
[OnyxUtils.METHOD.CLEAR]: {
344-
key: OnyxKey;
345-
value?: undefined;
346-
};
347-
[OnyxUtils.METHOD.MERGE_COLLECTION]: {
348-
key: CollectionKeyBase;
349-
value: OnyxMergeCollectionInput<CollectionKeyBase>;
350-
};
351-
[OnyxUtils.METHOD.SET_COLLECTION]: {
352-
key: CollectionKeyBase;
353-
value: OnyxMergeCollectionInput<CollectionKeyBase>;
354-
};
355-
};
329+
type OnyxMethodMap = typeof OnyxUtils.METHOD;
356330

357331
/**
358332
* OnyxUpdate type includes all onyx methods used in OnyxMethodValueMap.
359333
* If a new method is added to OnyxUtils.METHOD constant, it must be added to OnyxMethodValueMap type.
360334
* Otherwise it will show static type errors.
361335
*/
362-
type OnyxUpdate = {
363-
[Method in OnyxMethod]: {
364-
onyxMethod: Method;
365-
} & OnyxMethodValueMap[Method];
366-
}[OnyxMethod];
336+
type OnyxUpdate =
337+
// ⚠️ DO NOT CHANGE THIS TYPE, UNLESS YOU KNOW WHAT YOU ARE DOING. ⚠️
338+
| {
339+
[TKey in OnyxKey]:
340+
| {
341+
onyxMethod: typeof OnyxUtils.METHOD.SET;
342+
key: TKey;
343+
value: OnyxSetInput<TKey>;
344+
}
345+
| {
346+
onyxMethod: typeof OnyxUtils.METHOD.MULTI_SET;
347+
key: TKey;
348+
value: OnyxMultiSetInput;
349+
}
350+
| {
351+
onyxMethod: typeof OnyxUtils.METHOD.MERGE;
352+
key: TKey;
353+
value: OnyxMergeInput<TKey>;
354+
}
355+
| {
356+
onyxMethod: typeof OnyxUtils.METHOD.CLEAR;
357+
key: TKey;
358+
value?: undefined;
359+
};
360+
}[OnyxKey]
361+
| {
362+
[TKey in CollectionKeyBase]:
363+
| {
364+
onyxMethod: typeof OnyxUtils.METHOD.MERGE_COLLECTION;
365+
key: TKey;
366+
value: OnyxMergeCollectionInput<TKey>;
367+
}
368+
| {
369+
onyxMethod: typeof OnyxUtils.METHOD.SET_COLLECTION;
370+
key: TKey;
371+
value: OnyxSetCollectionInput<TKey>;
372+
};
373+
}[CollectionKeyBase];
367374

368375
/**
369376
* Represents the options used in `Onyx.set()` method.
@@ -474,6 +481,7 @@ export type {
474481
OnyxMultiSetInput,
475482
OnyxMergeInput,
476483
OnyxMergeCollectionInput,
484+
OnyxSetCollectionInput,
477485
OnyxMethod,
478486
OnyxMethodMap,
479487
OnyxUpdate,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"lint": "eslint .",
3333
"typecheck": "tsc --noEmit",
3434
"test": "jest",
35+
"test:types": "npm run build && tsc --noEmit --project tsconfig.test.json",
3536
"perf-test": "npx reassure",
3637
"build": "tsc -p tsconfig.build.json",
3738
"build:watch": "nodemon --watch lib --ext js,json,ts,tsx --exec \"npm run build && npm pack\"",

tests/types/OnyxUpdate.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type {OnyxUpdate} from '../../dist/types';
2+
import ONYX_KEYS from './setup';
3+
4+
const onyxUpdate: OnyxUpdate = {
5+
onyxMethod: 'set',
6+
key: ONYX_KEYS.TEST_KEY,
7+
value: 'string',
8+
};
9+
10+
const onyxUpdateError: OnyxUpdate = {
11+
onyxMethod: 'set',
12+
key: ONYX_KEYS.TEST_KEY,
13+
// @ts-expect-error TEST_KEY is a string, not a number
14+
value: 2,
15+
};
16+
17+
const onyxUpdateCollection: OnyxUpdate = {
18+
onyxMethod: 'mergecollection',
19+
key: ONYX_KEYS.COLLECTION.TEST_KEY,
20+
value: {
21+
[`${ONYX_KEYS.COLLECTION.TEST_KEY}1`]: {
22+
str: 'test',
23+
},
24+
[`${ONYX_KEYS.COLLECTION.TEST_KEY}2`]: {
25+
str: 'test2',
26+
},
27+
},
28+
};
29+
30+
// @ts-expect-error COLLECTION.TEST_KEY is an object, not a number
31+
const onyxUpdateCollectionError: OnyxUpdate = {
32+
onyxMethod: 'mergecollection',
33+
key: ONYX_KEYS.COLLECTION.TEST_KEY,
34+
value: {
35+
[`${ONYX_KEYS.COLLECTION.TEST_KEY}1`]: 2,
36+
},
37+
};
38+
39+
const onyxUpdateCollectionError2: OnyxUpdate = {
40+
onyxMethod: 'mergecollection',
41+
key: ONYX_KEYS.COLLECTION.TEST_KEY,
42+
value: {
43+
[`${ONYX_KEYS.COLLECTION.TEST_KEY}2`]: {
44+
// @ts-expect-error nonExistingKey is not a valid key
45+
nonExistingKey: 'test2',
46+
},
47+
},
48+
};
49+
50+
// @ts-expect-error COLLECTION.TEST_KEY is invalid key, it is missing the suffix
51+
const onyxUpdateCollectionError3: OnyxUpdate = {
52+
onyxMethod: 'mergecollection',
53+
key: ONYX_KEYS.COLLECTION.TEST_KEY,
54+
value: {
55+
[ONYX_KEYS.COLLECTION.TEST_KEY]: {
56+
str: 'test2',
57+
},
58+
},
59+
};

tests/types/mergeCollection.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Onyx from '../../dist/Onyx';
2+
import ONYX_KEYS from './setup';
3+
4+
Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, {
5+
test_1: {
6+
str: 'test3',
7+
},
8+
test_2: {
9+
str: 'test4',
10+
},
11+
test_3: {
12+
str: 'test5',
13+
},
14+
});
15+
16+
Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, {
17+
// @ts-expect-error COLLECTION.TEST_KEY is invalid key, it is missing the suffix
18+
test_: {
19+
str: 'test3',
20+
},
21+
test_2: {
22+
str: 'test4',
23+
},
24+
// @ts-expect-error COLLECTION.TEST_KEY is object, not a number
25+
test_3: 2,
26+
});

0 commit comments

Comments
 (0)