diff --git a/.changeset/breezy-flies-exist.md b/.changeset/breezy-flies-exist.md new file mode 100644 index 00000000000..16b65c30bee --- /dev/null +++ b/.changeset/breezy-flies-exist.md @@ -0,0 +1,5 @@ +--- +"@firebase/firestore": minor +--- + +Changing UpdateData to expand support for types with index signatures. diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 3b701b64452..894b9efc4fb 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -14,7 +14,7 @@ export function addDoc(reference // @public export type AddPrefixToKeys> = { - [K in keyof T & string as `${Prefix}.${K}`]+?: T[K]; + [K in keyof T & string as `${Prefix}.${K}`]+?: string extends K ? any : T[K]; }; // @public diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index fca8f53586c..ada53f25a59 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -14,7 +14,7 @@ export function addDoc(reference // @public export type AddPrefixToKeys> = { - [K in keyof T & string as `${Prefix}.${K}`]+?: T[K]; + [K in keyof T & string as `${Prefix}.${K}`]+?: string extends K ? any : T[K]; }; // @public diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 462635b0f85..0d1398c8f70 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -2209,7 +2209,7 @@ Returns a new map where every key is prefixed with the outer key appended to a d ```typescript export declare type AddPrefixToKeys> = { - [K in keyof T & string as `${Prefix}.${K}`]+?: T[K]; + [K in keyof T & string as `${Prefix}.${K}`]+?: string extends K ? any : T[K]; }; ``` diff --git a/docs-devsite/firestore_lite.md b/docs-devsite/firestore_lite.md index b87e2417b93..e6ba851bbd7 100644 --- a/docs-devsite/firestore_lite.md +++ b/docs-devsite/firestore_lite.md @@ -1424,7 +1424,7 @@ Returns a new map where every key is prefixed with the outer key appended to a d ```typescript export declare type AddPrefixToKeys> = { - [K in keyof T & string as `${Prefix}.${K}`]+?: T[K]; + [K in keyof T & string as `${Prefix}.${K}`]+?: string extends K ? any : T[K]; }; ``` diff --git a/packages/firestore/src/lite-api/types.ts b/packages/firestore/src/lite-api/types.ts index 4801e587836..3c697e63b41 100644 --- a/packages/firestore/src/lite-api/types.ts +++ b/packages/firestore/src/lite-api/types.ts @@ -66,7 +66,22 @@ export type AddPrefixToKeys< T extends Record > = // Remap K => Prefix.K. See https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as - { [K in keyof T & string as `${Prefix}.${K}`]+?: T[K] }; + + // `string extends K : ...` is used to detect index signatures + // like `{[key: string]: bool}`. We map these properties to type `any` + // because a field path like `foo.[string]` will match `foo.bar` or a + // sub-path `foo.bar.baz`. Because it matches a sub-path, we have to + // make this type `any` to allow for any types of the sub-path property. + // This is a significant downside to using index signatures in types for `T` + // for `UpdateData`. + + { + /* eslint-disable @typescript-eslint/no-explicit-any */ + [K in keyof T & string as `${Prefix}.${K}`]+?: string extends K + ? any + : T[K]; + /* eslint-enable @typescript-eslint/no-explicit-any */ + }; /** * Given a union type `U = T1 | T2 | ...`, returns an intersected type diff --git a/packages/firestore/test/unit/lite-api/types.test.ts b/packages/firestore/test/unit/lite-api/types.test.ts index d775f1fe107..55232aeee60 100644 --- a/packages/firestore/test/unit/lite-api/types.test.ts +++ b/packages/firestore/test/unit/lite-api/types.test.ts @@ -441,23 +441,101 @@ describe('UpdateData - v9', () => { } }; - // preserves type - failure - _ = { - // @ts-expect-error - 'indexed.bar': false, - // @ts-expect-error - 'indexed.baz': 'string' + expect(true).to.be.true; + }); + }); + + // v10 tests cover new scenarios that are fixed for v10 + describe('UpdateData - v10', () => { + interface MyV10ServerType { + booleanProperty: boolean; + + // index signatures nested 1 layer deep + indexed: { + [name: string]: { + booleanProperty: boolean; + numberProperty: number; + }; }; - // preserves properties of nested objects - failure - _ = { - 'indexed.bar': { - // @ts-expect-error - booleanProperty: 'string' - } + // index signatures nested 2 layers deep + layer: { + indexed: { + [name: string]: { + booleanProperty: boolean; + numberProperty: number; + }; + }; }; + } - expect(true).to.be.true; + describe('given nested objects with index properties', () => { + it('supports object replacement at each layer (with partial)', () => { + // This unexpectidly fails in v9 when the object has index signature nested + // two layers deep (e.g. layer.indexed.[name]). + const _: UpdateData = { + indexed: { + bar: {}, + baz: {} + } + }; + + expect(true).to.be.true; + }); + + it('allows dot notation for nested index types', () => { + let _: UpdateData; + + // v10 allows 3 layers of dot notation + + // allows the property + _ = { + 'indexed.bar.booleanProperty': true + }; + + _ = { + 'indexed.bar.numberProperty': 1 + }; + + // does not enforce type + _ = { + 'indexed.bar.booleanProperty': 'string value is not rejected' + }; + + _ = { + 'indexed.bar.numberProperty': 'string value is not rejected' + }; + + // rejects properties that don't exist + _ = { + 'indexed.bar.unknown': 'string value is not rejected' + }; + + expect(true).to.be.true; + }); + + it('allows dot notation for nested index types that are 2 layers deep', () => { + let _: UpdateData; + + // v10 3 layers with dot notation + + // allows the property + _ = { + 'layer.indexed.bar.booleanProperty': true + }; + + // allows the property, but does not enforce type + _ = { + 'layer.indexed.bar.booleanProperty': 'string value is not rejected' + }; + + // Allows unknown properties in sub types + _ = { + 'layer.indexed.bar.unknownProperty': 'This just allows anything' + }; + + expect(true).to.be.true; + }); }); }); });