Skip to content

Commit be8a42f

Browse files
author
Brian Chen
committed
add support for union fields
1 parent 45140ad commit be8a42f

File tree

8 files changed

+88
-15
lines changed

8 files changed

+88
-15
lines changed

.changeset/clean-cameras-check.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@firebase/firestore': minor
33
---
44

5-
Fixed a bug where UpdateData did not recognize optional, dot-separated string fields
5+
Fixed a bug where `UpdateData` did not recognize optional, dot-separated string fields.

common/api-review/firestore-lite.api.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export class Bytes {
3232
toUint8Array(): Uint8Array;
3333
}
3434

35+
// @public
36+
export type ChildUpdateFields<T, K extends string> = T extends Record<string, any> ? AddPrefixToKeys<K, UpdateData<T>> : never;
37+
3538
// @public
3639
export function collection(firestore: Firestore, path: string, ...pathSegments: string[]): CollectionReference<DocumentData>;
3740

@@ -191,7 +194,7 @@ export { LogLevel }
191194

192195
// @public
193196
export type NestedUpdateFields<T extends Record<string, unknown>> = UnionToIntersection<{
194-
[K in keyof T & string]: T[K] extends Record<string, unknown> ? AddPrefixToKeys<K, UpdateData<T[K]>> : never;
197+
[K in keyof T & string]: ChildUpdateFields<T[K], K>;
195198
}[keyof T & string]>;
196199

197200
// @public
@@ -332,7 +335,7 @@ export class Transaction {
332335
export type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
333336

334337
// @public
335-
export type UpdateData<T> = T extends Primitive ? T : T extends Map<infer K, infer V> ? Map<UpdateData<K>, UpdateData<V>> : T extends {} ? {
338+
export type UpdateData<T> = T extends Primitive ? T : T extends {} ? {
336339
[K in keyof T]?: UpdateData<T[K]> | FieldValue;
337340
} & NestedUpdateFields<T> : Partial<T>;
338341

common/api-review/firestore.api.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export class Bytes {
3535
// @public
3636
export const CACHE_SIZE_UNLIMITED = -1;
3737

38+
// @public
39+
export type ChildUpdateFields<T, K extends string> = T extends Record<string, any> ? AddPrefixToKeys<K, UpdateData<T>> : never;
40+
3841
// @public
3942
export function clearIndexedDbPersistence(firestore: Firestore): Promise<void>;
4043

@@ -265,7 +268,7 @@ export function namedQuery(firestore: Firestore, name: string): Promise<Query |
265268

266269
// @public
267270
export type NestedUpdateFields<T extends Record<string, unknown>> = UnionToIntersection<{
268-
[K in keyof T & string]: T[K] extends Record<string, unknown> ? AddPrefixToKeys<K, UpdateData<T[K]>> : never;
271+
[K in keyof T & string]: ChildUpdateFields<T[K], K>;
269272
}[keyof T & string]>;
270273

271274
// @public
@@ -481,7 +484,7 @@ export interface Unsubscribe {
481484
}
482485

483486
// @public
484-
export type UpdateData<T> = T extends Primitive ? T : T extends Map<infer K, infer V> ? Map<UpdateData<K>, UpdateData<V>> : T extends {} ? {
487+
export type UpdateData<T> = T extends Primitive ? T : T extends {} ? {
485488
[K in keyof T]?: UpdateData<T[K]> | FieldValue;
486489
} & NestedUpdateFields<T> : Partial<T>;
487490

packages/firestore/lite/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export {
8282
export {
8383
Primitive,
8484
NestedUpdateFields,
85+
ChildUpdateFields,
8586
AddPrefixToKeys,
8687
UnionToIntersection
8788
} from '../src/lite-api/types';

packages/firestore/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export { AbstractUserDataWriter } from './lite-api/user_data_writer';
136136
export {
137137
Primitive,
138138
NestedUpdateFields,
139+
ChildUpdateFields,
139140
AddPrefixToKeys,
140141
UnionToIntersection
141142
} from '../src/lite-api/types';

packages/firestore/src/lite-api/reference.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,11 @@ export type WithFieldValue<T> = T extends Primitive
7676
* reference nested fields within the document. FieldValues can be passed in
7777
* as property values.
7878
*/
79-
export type UpdateData<T> = T extends undefined
80-
? never
81-
: T extends Primitive
79+
export type UpdateData<T> = T extends Primitive
8280
? T
8381
: T extends {}
8482
? { [K in keyof T]?: UpdateData<T[K]> | FieldValue } & NestedUpdateFields<T>
8583
: Partial<T>;
86-
8784
/**
8885
* An options object that configures the behavior of {@link @firebase/firestore/lite#(setDoc:1)}, {@link
8986
* @firebase/firestore/lite#(WriteBatch.set:1)} and {@link @firebase/firestore/lite#(Transaction.set:1)} calls. These calls can be

packages/firestore/src/lite-api/types.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,26 @@ export type NestedUpdateFields<T extends Record<string, unknown>> =
3535
UnionToIntersection<
3636
{
3737
// Check that T[K] extends Record to only allow nesting for map values.
38-
[K in keyof T & string]: T[K] extends Record<string, unknown> | undefined
39-
? // Recurse into the map and add the prefix in front of each key
40-
// (e.g. Prefix 'bar.' to create: 'bar.baz' and 'bar.qux'.
41-
AddPrefixToKeys<K, UpdateData<T[K]>>
42-
: // TypedUpdateData is always a map of values.
43-
never;
38+
// Union with `undefined` to allow for optional nested fields
39+
[K in keyof T & string]: ChildUpdateFields<T[K], K>;
4440
}[keyof T & string] // Also include the generated prefix-string keys.
4541
>;
4642

43+
/**
44+
* Helper for calculating the nested fields for a given type T. This is needed
45+
* to distribute union types such as `undefined | {...}` (happens for optional
46+
* props) or `{a: A} | {b: B}`.
47+
* https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types
48+
*/
49+
export type ChildUpdateFields<T, K extends string> =
50+
// Only allow nesting for map values
51+
T extends Record<string, any>
52+
? // Recurse into the map and add the prefix in front of each key
53+
// (e.g. Prefix 'bar.' to create: 'bar.baz' and 'bar.qux'.
54+
AddPrefixToKeys<K, UpdateData<T>>
55+
: // UpdateData is always a map of values.
56+
never;
57+
4758
/**
4859
* Returns a new map where every key is prefixed with the outer key appended
4960
* to a dot.

packages/firestore/test/lite/integration.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,63 @@ describe('withConverter() support', () => {
15851585
});
15861586
});
15871587

1588+
it('supports union fields', () => {
1589+
interface TestObjectUnion {
1590+
optionalStr?: string;
1591+
nested?:
1592+
| {
1593+
requiredStr: string;
1594+
}
1595+
| { requiredStrObject: string };
1596+
}
1597+
1598+
const testConverterUnion = {
1599+
toFirestore(testObj: WithFieldValue<TestObjectUnion>) {
1600+
return { ...testObj };
1601+
},
1602+
fromFirestore(snapshot: QueryDocumentSnapshot): TestObjectUnion {
1603+
const data = snapshot.data();
1604+
return {
1605+
optionalStr: data.optionalStr,
1606+
nested: data.nested
1607+
};
1608+
}
1609+
};
1610+
1611+
return withTestDocAndInitialData(initialData, async docRef => {
1612+
const testDocRef: DocumentReference<TestObjectUnion> =
1613+
docRef.withConverter(testConverterUnion);
1614+
1615+
await updateDoc(testDocRef, {
1616+
optionalStr: 'foo'
1617+
});
1618+
await updateDoc(testDocRef, {
1619+
'optionalStr': 'foo'
1620+
});
1621+
1622+
await updateDoc(testDocRef, {
1623+
nested: {
1624+
requiredStr: 'foo'
1625+
}
1626+
});
1627+
await updateDoc(testDocRef, {
1628+
'nested.requiredStr': 'foo'
1629+
});
1630+
await updateDoc(testDocRef, {
1631+
// @ts-expect-error
1632+
'nested.requiredStr': 1
1633+
});
1634+
await updateDoc(testDocRef, {
1635+
'nested.requiredStrObject': 'foo'
1636+
});
1637+
1638+
await updateDoc(testDocRef, {
1639+
// @ts-expect-error
1640+
'nested.requiredStringObject': 1
1641+
});
1642+
});
1643+
});
1644+
15881645
it('checks for nonexistent fields', () => {
15891646
return withTestDocAndInitialData(initialData, async docRef => {
15901647
const testDocRef: DocumentReference<TestObject> =

0 commit comments

Comments
 (0)