Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: UpdateData<T> to allow indexed types or Record for T. #7887

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-geckos-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@firebase/firestore": patch
---

UpdateData<T> allows indexed types or Record<X, T> for T.
9 changes: 7 additions & 2 deletions common/api-review/firestore-lite.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export class Bytes {
toUint8Array(): Uint8Array;
}

// @public
export type ChildTypes<T> = T extends Record<string, unknown> ? {
[K in keyof T & string]: ChildTypes<T[K]>;
}[keyof T & string] | T : T;

// @public
export type ChildUpdateFields<K extends string, V> = V extends Record<string, unknown> ? AddPrefixToKeys<K, UpdateData<V>> : never;

Expand Down Expand Up @@ -259,7 +264,7 @@ export { LogLevel }

// @public
export type NestedUpdateFields<T extends Record<string, unknown>> = UnionToIntersection<{
[K in keyof T & string]: ChildUpdateFields<K, T[K]>;
[K in keyof T & string]: string extends K ? never : ChildUpdateFields<K, T[K]>;
}[keyof T & string]>;

// @public
Expand Down Expand Up @@ -451,7 +456,7 @@ export type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never

// @public
export type UpdateData<T> = T extends Primitive ? T : T extends {} ? {
[K in keyof T]?: UpdateData<T[K]> | FieldValue;
[K in keyof T]?: string extends K ? PartialWithFieldValue<ChildTypes<T[K]>> : UpdateData<T[K]> | FieldValue;
} & NestedUpdateFields<T> : Partial<T>;

// @public
Expand Down
9 changes: 7 additions & 2 deletions common/api-review/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export class Bytes {
// @public
export const CACHE_SIZE_UNLIMITED = -1;

// @public
export type ChildTypes<T> = T extends Record<string, unknown> ? {
[K in keyof T & string]: ChildTypes<T[K]>;
}[keyof T & string] | T : T;

// @public
export type ChildUpdateFields<K extends string, V> = V extends Record<string, unknown> ? AddPrefixToKeys<K, UpdateData<V>> : never;

Expand Down Expand Up @@ -413,7 +418,7 @@ export function namedQuery(firestore: Firestore, name: string): Promise<Query |

// @public
export type NestedUpdateFields<T extends Record<string, unknown>> = UnionToIntersection<{
[K in keyof T & string]: ChildUpdateFields<K, T[K]>;
[K in keyof T & string]: string extends K ? never : ChildUpdateFields<K, T[K]>;
}[keyof T & string]>;

// @public
Expand Down Expand Up @@ -732,7 +737,7 @@ export interface Unsubscribe {

// @public
export type UpdateData<T> = T extends Primitive ? T : T extends {} ? {
[K in keyof T]?: UpdateData<T[K]> | FieldValue;
[K in keyof T]?: string extends K ? PartialWithFieldValue<ChildTypes<T[K]>> : UpdateData<T[K]> | FieldValue;
} & NestedUpdateFields<T> : Partial<T>;

// @public
Expand Down
17 changes: 15 additions & 2 deletions docs-devsite/firestore_.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ https://github.com/firebase/firebase-js-sdk
| [AggregateFieldType](./firestore_.md#aggregatefieldtype) | The union of all <code>AggregateField</code> types that are supported by Firestore. |
| [AggregateSpecData](./firestore_.md#aggregatespecdata) | A type whose keys are taken from an <code>AggregateSpec</code>, and whose values are the result of the aggregation performed by the corresponding <code>AggregateField</code> from the input <code>AggregateSpec</code>. |
| [AggregateType](./firestore_.md#aggregatetype) | Union type representing the aggregate type to be performed. |
| [ChildTypes](./firestore_.md#childtypes) | For the given type, return a union type of T and the types of all child properties of T. |
| [ChildUpdateFields](./firestore_.md#childupdatefields) | Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as <code>undefined &#124; {...}</code> (happens for optional props) or <code>{a: A} &#124; {b: B}</code>.<!-- -->In this use case, <code>V</code> is used to distribute the union types of <code>T[K]</code> on <code>Record</code>, since <code>T[K]</code> is evaluated as an expression and not distributed.<!-- -->See https://www.typescriptlang.org/docs/handbook/advanced-types.html\#distributive-conditional-types |
| [DocumentChangeType](./firestore_.md#documentchangetype) | The type of a <code>DocumentChange</code> may be 'added', 'removed', or 'modified'. |
| [FirestoreErrorCode](./firestore_.md#firestoreerrorcode) | The set of Firestore status codes. The codes are the same at the ones exposed by gRPC here: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md<!-- -->Possible values: - 'cancelled': The operation was cancelled (typically by the caller). - 'unknown': Unknown error or an error from a different error domain. - 'invalid-argument': Client specified an invalid argument. Note that this differs from 'failed-precondition'. 'invalid-argument' indicates arguments that are problematic regardless of the state of the system (e.g. an invalid field name). - 'deadline-exceeded': Deadline expired before operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long enough for the deadline to expire. - 'not-found': Some requested document was not found. - 'already-exists': Some document that we attempted to create already exists. - 'permission-denied': The caller does not have permission to execute the specified operation. - 'resource-exhausted': Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. - 'failed-precondition': Operation was rejected because the system is not in a state required for the operation's execution. - 'aborted': The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - 'out-of-range': Operation was attempted past the valid range. - 'unimplemented': Operation is not implemented or not supported/enabled. - 'internal': Internal errors. Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. - 'unavailable': The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff. - 'data-loss': Unrecoverable data loss or corruption. - 'unauthenticated': The request does not have valid authentication credentials for the operation. |
Expand Down Expand Up @@ -2505,6 +2506,18 @@ Union type representing the aggregate type to be performed.
export declare type AggregateType = 'count' | 'avg' | 'sum';
```

## ChildTypes

For the given type, return a union type of T and the types of all child properties of T.

<b>Signature:</b>

```typescript
export declare type ChildTypes<T> = T extends Record<string, unknown> ? {
[K in keyof T & string]: ChildTypes<T[K]>;
}[keyof T & string] | T : T;
```

## ChildUpdateFields

Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as `undefined | {...}` (happens for optional props) or `{a: A} | {b: B}`<!-- -->.
Expand Down Expand Up @@ -2569,7 +2582,7 @@ For each field (e.g. 'bar'), find all nested keys (e.g. {<!-- -->'bar.baz': T1,

```typescript
export declare type NestedUpdateFields<T extends Record<string, unknown>> = UnionToIntersection<{
[K in keyof T & string]: ChildUpdateFields<K, T[K]>;
[K in keyof T & string]: string extends K ? never : ChildUpdateFields<K, T[K]>;
}[keyof T & string]>;
```

Expand Down Expand Up @@ -2691,7 +2704,7 @@ Update data (for use with [updateDoc()](./firestore_.md#updatedoc_51a65e3)<!-- -

```typescript
export declare type UpdateData<T> = T extends Primitive ? T : T extends {} ? {
[K in keyof T]?: UpdateData<T[K]> | FieldValue;
[K in keyof T]?: string extends K ? PartialWithFieldValue<ChildTypes<T[K]>> : UpdateData<T[K]> | FieldValue;
} & NestedUpdateFields<T> : Partial<T>;
```

Expand Down
17 changes: 15 additions & 2 deletions docs-devsite/firestore_lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ https://github.com/firebase/firebase-js-sdk
| [AggregateFieldType](./firestore_lite.md#aggregatefieldtype) | The union of all <code>AggregateField</code> types that are supported by Firestore. |
| [AggregateSpecData](./firestore_lite.md#aggregatespecdata) | A type whose keys are taken from an <code>AggregateSpec</code>, and whose values are the result of the aggregation performed by the corresponding <code>AggregateField</code> from the input <code>AggregateSpec</code>. |
| [AggregateType](./firestore_lite.md#aggregatetype) | Union type representing the aggregate type to be performed. |
| [ChildTypes](./firestore_lite.md#childtypes) | For the given type, return a union type of T and the types of all child properties of T. |
| [ChildUpdateFields](./firestore_lite.md#childupdatefields) | Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as <code>undefined &#124; {...}</code> (happens for optional props) or <code>{a: A} &#124; {b: B}</code>.<!-- -->In this use case, <code>V</code> is used to distribute the union types of <code>T[K]</code> on <code>Record</code>, since <code>T[K]</code> is evaluated as an expression and not distributed.<!-- -->See https://www.typescriptlang.org/docs/handbook/advanced-types.html\#distributive-conditional-types |
| [FirestoreErrorCode](./firestore_lite.md#firestoreerrorcode) | The set of Firestore status codes. The codes are the same at the ones exposed by gRPC here: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md<!-- -->Possible values: - 'cancelled': The operation was cancelled (typically by the caller). - 'unknown': Unknown error or an error from a different error domain. - 'invalid-argument': Client specified an invalid argument. Note that this differs from 'failed-precondition'. 'invalid-argument' indicates arguments that are problematic regardless of the state of the system (e.g. an invalid field name). - 'deadline-exceeded': Deadline expired before operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long enough for the deadline to expire. - 'not-found': Some requested document was not found. - 'already-exists': Some document that we attempted to create already exists. - 'permission-denied': The caller does not have permission to execute the specified operation. - 'resource-exhausted': Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. - 'failed-precondition': Operation was rejected because the system is not in a state required for the operation's execution. - 'aborted': The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - 'out-of-range': Operation was attempted past the valid range. - 'unimplemented': Operation is not implemented or not supported/enabled. - 'internal': Internal errors. Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. - 'unavailable': The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff. - 'data-loss': Unrecoverable data loss or corruption. - 'unauthenticated': The request does not have valid authentication credentials for the operation. |
| [NestedUpdateFields](./firestore_lite.md#nestedupdatefields) | For each field (e.g. 'bar'), find all nested keys (e.g. {<!-- -->'bar.baz': T1, 'bar.qux': T2<!-- -->}<!-- -->). Intersect them together to make a single map containing all possible keys that are all marked as optional |
Expand Down Expand Up @@ -1612,6 +1613,18 @@ Union type representing the aggregate type to be performed.
export declare type AggregateType = 'count' | 'avg' | 'sum';
```

## ChildTypes

For the given type, return a union type of T and the types of all child properties of T.

<b>Signature:</b>

```typescript
export declare type ChildTypes<T> = T extends Record<string, unknown> ? {
[K in keyof T & string]: ChildTypes<T[K]>;
}[keyof T & string] | T : T;
```

## ChildUpdateFields

Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as `undefined | {...}` (happens for optional props) or `{a: A} | {b: B}`<!-- -->.
Expand Down Expand Up @@ -1646,7 +1659,7 @@ For each field (e.g. 'bar'), find all nested keys (e.g. {<!-- -->'bar.baz': T1,

```typescript
export declare type NestedUpdateFields<T extends Record<string, unknown>> = UnionToIntersection<{
[K in keyof T & string]: ChildUpdateFields<K, T[K]>;
[K in keyof T & string]: string extends K ? never : ChildUpdateFields<K, T[K]>;
}[keyof T & string]>;
```

Expand Down Expand Up @@ -1746,7 +1759,7 @@ Update data (for use with [updateDoc()](./firestore_.md#updatedoc_51a65e3)<!-- -

```typescript
export declare type UpdateData<T> = T extends Primitive ? T : T extends {} ? {
[K in keyof T]?: UpdateData<T[K]> | FieldValue;
[K in keyof T]?: string extends K ? PartialWithFieldValue<ChildTypes<T[K]>> : UpdateData<T[K]> | FieldValue;
} & NestedUpdateFields<T> : Partial<T>;
```

Expand Down
1 change: 1 addition & 0 deletions packages/firestore/lite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export {
Primitive,
NestedUpdateFields,
ChildUpdateFields,
ChildTypes,
AddPrefixToKeys,
UnionToIntersection
} from '../src/lite-api/types';
Expand Down
1 change: 1 addition & 0 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export { AbstractUserDataWriter } from './lite-api/user_data_writer';
export {
AddPrefixToKeys,
ChildUpdateFields,
ChildTypes,
NestedUpdateFields,
Primitive,
UnionToIntersection
Expand Down
12 changes: 10 additions & 2 deletions packages/firestore/src/lite-api/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { Firestore } from './database';
import { FieldPath } from './field_path';
import { FieldValue } from './field_value';
import { FirestoreDataConverter } from './snapshot';
import { NestedUpdateFields, Primitive } from './types';
import { ChildTypes, NestedUpdateFields, Primitive } from './types';

/**
* Document data (for use with {@link @firebase/firestore/lite#(setDoc:1)}) consists of fields mapped to
Expand Down Expand Up @@ -83,8 +83,16 @@ export type WithFieldValue<T> =
export type UpdateData<T> = T extends Primitive
? T
: T extends {}
? { [K in keyof T]?: UpdateData<T[K]> | FieldValue } & NestedUpdateFields<T>
? {
// If `string extends K`, this is an index signature like
// `{[key: string]: { foo: bool }}`. In the generated UpdateData
// indexed properties can match their type or any child types.
[K in keyof T]?: string extends K
? PartialWithFieldValue<ChildTypes<T[K]>>
: UpdateData<T[K]> | FieldValue;
} & NestedUpdateFields<T>
: Partial<T>;

/**
* An options object that configures the behavior of {@link @firebase/firestore/lite#(setDoc:1)}, {@link
* @firebase/firestore/lite#(WriteBatch.set:1)} and {@link @firebase/firestore/lite#(Transaction.set:1)} calls. These calls can be
Expand Down
23 changes: 21 additions & 2 deletions packages/firestore/src/lite-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ export type Primitive = string | number | boolean | undefined | null;
export type NestedUpdateFields<T extends Record<string, unknown>> =
UnionToIntersection<
{
[K in keyof T & string]: ChildUpdateFields<K, T[K]>;
// If `string extends K`, this is an index signature like
// `{[key: string]: { foo: bool }}`. We map these properties to
// `never`, which prevents prefixing a nested key with `[string]`.
// We don't want to generate a field like `[string].foo: bool`.
[K in keyof T & string]: string extends K
? never
: ChildUpdateFields<K, T[K]>;
}[keyof T & string] // Also include the generated prefix-string keys.
>;

Expand All @@ -57,6 +63,18 @@ export type ChildUpdateFields<K extends string, V> =
: // UpdateData is always a map of values.
never;

/**
* For the given type, return a union type of T
* and the types of all child properties of T.
*/
export type ChildTypes<T> = T extends Record<string, unknown>
?
| {
[K in keyof T & string]: ChildTypes<T[K]>;
}[keyof T & string]
| T
: T;

/**
* Returns a new map where every key is prefixed with the outer key appended
* to a dot.
Expand All @@ -78,7 +96,8 @@ export type AddPrefixToKeys<
{
/* eslint-disable @typescript-eslint/no-explicit-any */
[K in keyof T & string as `${Prefix}.${K}`]+?: string extends K
? any
? // TODO(b/316955294): Replace `any` with `ChildTypes<T[K]>` (breaking change).
any
: T[K];
/* eslint-enable @typescript-eslint/no-explicit-any */
};
Expand Down
Loading
Loading