From 27b46bcb007d1a9741bbe2169d54b22e4eced417 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Thu, 12 Feb 2026 11:09:03 -0500 Subject: [PATCH 1/2] feat: add `isType` expression Adds support for the `isType` expression. Ported from https://github.com/firebase/firebase-js-sdk/pull/9484. --- api-report/firestore.api.md | 12 ++++ dev/src/pipelines/expression.ts | 97 +++++++++++++++++++++++++++++++++ dev/src/pipelines/index.ts | 2 + dev/system-test/pipeline.ts | 69 +++++++++++++++++++++++ types/firestore.d.ts | 88 ++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+) diff --git a/api-report/firestore.api.md b/api-report/firestore.api.md index aeab4db41..ce7261878 100644 --- a/api-report/firestore.api.md +++ b/api-report/firestore.api.md @@ -1014,6 +1014,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData ifError(catchValue: unknown): FunctionExpression; isAbsent(): BooleanExpression; isError(): BooleanExpression; + isType(type: Type): BooleanExpression; join(delimiterExpression: Expression): Expression; join(delimiter: string): Expression; length(): FunctionExpression; @@ -1501,6 +1502,12 @@ function isAbsent(field: string): BooleanExpression; // @beta function isError(value: Expression): BooleanExpression; +// @beta +function isType(fieldName: string, type: Type): BooleanExpression; + +// @beta +function isType(expression: Expression, type: Type): BooleanExpression; + // @beta function join(arrayFieldName: string, delimiter: string): Expression; @@ -1901,6 +1908,8 @@ declare namespace Pipelines { currentTimestamp, arrayConcat, type, + isType, + Type, timestampTruncate, split } @@ -2582,6 +2591,9 @@ function trim(fieldName: string, valueToTrim?: string | Expression): FunctionExp // @beta function trim(stringExpression: Expression, valueToTrim?: string | Expression): FunctionExpression; +// @beta +type Type = 'null' | 'array' | 'boolean' | 'bytes' | 'timestamp' | 'geo_point' | 'number' | 'int32' | 'int64' | 'float64' | 'decimal128' | 'map' | 'reference' | 'string' | 'vector' | 'max_key' | 'min_key' | 'object_id' | 'regex' | 'request_timestamp'; + // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // diff --git a/dev/src/pipelines/expression.ts b/dev/src/pipelines/expression.ts index 081b822bb..fb3fbea01 100644 --- a/dev/src/pipelines/expression.ts +++ b/dev/src/pipelines/expression.ts @@ -29,6 +29,39 @@ import { import {HasUserData, Serializer, validateUserInput} from '../serializer'; import {cast} from '../util'; +/** + * @beta + * + * An enumeration of the different types generated by the Firestore backend. + * + * + */ +export type Type = + | 'null' + | 'array' + | 'boolean' + | 'bytes' + | 'timestamp' + | 'geo_point' + | 'number' + | 'int32' + | 'int64' + | 'float64' + | 'decimal128' + | 'map' + | 'reference' + | 'string' + | 'vector' + | 'max_key' + | 'min_key' + | 'object_id' + | 'regex' + | 'request_timestamp'; + /** * @beta * Represents an expression that can be evaluated to a value within the execution of a `Pipeline`. @@ -2349,6 +2382,28 @@ export abstract class Expression return new FunctionExpression('type', [this]); } + /** + * @beta + * Creates an expression that checks if the result of this expression is of the given type. + * + * @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data. + * + * @example + * ```typescript + * // Check if the 'price' field is specifically an integer (not just 'number') + * field('price').isType('int64'); + * ``` + * + * @param type - The type to check for. + * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. + */ + isType(type: Type): BooleanExpression { + return new FunctionExpression('is_type', [ + this, + constant(type), + ]).asBoolean(); + } + // TODO(new-expression): Add new expression method definitions above this line /** @@ -8021,6 +8076,48 @@ export function type( return fieldOrExpression(fieldNameOrExpression).type(); } +/** + * @beta + * Creates an expression that checks if the value in the specified field is of the given type. + * + * @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data. + * + * @example + * ```typescript + * // Check if the 'price' field is a floating point number (evaluating to true inside pipeline conditionals) + * isType('price', 'float64'); + * ``` + * + * @param fieldName - The name of the field to check. + * @param type - The type to check for. + * @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise. + */ +export function isType(fieldName: string, type: Type): BooleanExpression; + +/** + * @beta + * Creates an expression that checks if the result of an expression is of the given type. + * + * @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data. + * + * @example + * ```typescript + * // Check if the result of a calculation is a number + * isType(add('count', 1), 'number') + * ``` + * + * @param expression - The expression to check. + * @param type - The type to check for. + * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. + */ +export function isType(expression: Expression, type: Type): BooleanExpression; +export function isType( + fieldNameOrExpression: string | Expression, + type: Type, +): BooleanExpression { + return fieldOrExpression(fieldNameOrExpression).isType(type); +} + // TODO(new-expression): Add new top-level expression function definitions above this line /** diff --git a/dev/src/pipelines/index.ts b/dev/src/pipelines/index.ts index 79cd1a389..387408fbe 100644 --- a/dev/src/pipelines/index.ts +++ b/dev/src/pipelines/index.ts @@ -122,6 +122,8 @@ export { currentTimestamp, arrayConcat, type, + isType, + Type, timestampTruncate, split, // TODO(new-expression): Add new expression exports above this line diff --git a/dev/system-test/pipeline.ts b/dev/system-test/pipeline.ts index efc5ff470..066029dac 100644 --- a/dev/system-test/pipeline.ts +++ b/dev/system-test/pipeline.ts @@ -116,6 +116,7 @@ import { currentTimestamp, arrayConcat, type, + isType, timestampTruncate, split, // TODO(new-expression): add new expression imports above this line @@ -4208,6 +4209,74 @@ describe.skipClassic('Pipeline class', () => { }); }); + it('supports isType', async () => { + const result = await firestore + .pipeline() + .collection(randomCol.path) + .replaceWith( + map({ + int: constant(1), + float: constant(1.1), + str: constant('a string'), + bool: constant(true), + null: constant(null), + geoPoint: constant(new GeoPoint(0.1, 0.2)), + timestamp: constant(new Timestamp(123456, 0)), + bytes: constant(new Uint8Array([1, 2, 3])), + docRef: constant(firestore.doc(`${randomCol.path}/bar`)), + vector: constant(FieldValue.vector([1, 2, 3])), + map: map({ + numberK: 1, + stringK: 'a string', + }), + array: array([1, '2', true]), + }), + ) + .select( + isType(field('int'), 'int64').as('isInt64'), + isType(field('int'), 'number').as('isInt64IsNumber'), + isType(field('int'), 'decimal128').as('isInt64IsDecimal128'), + field('float').isType('float64').as('isFloat64'), + field('float').isType('number').as('isFloat64IsNumber'), + field('float').isType('decimal128').as('isFloat64IsDecimal128'), + isType('str', 'string').as('isStr'), + isType('int', 'string').as('isNumStr'), + field('bool').isType('boolean').as('isBool'), + isType('null', 'null').as('isNull'), + field('geoPoint').isType('geo_point').as('isGeoPoint'), + isType('timestamp', 'timestamp').as('isTimestamp'), + field('bytes').isType('bytes').as('isBytes'), + isType('docRef', 'reference').as('isDocRef'), + field('vector').isType('vector').as('isVector'), + isType('map', 'map').as('isMap'), + field('array').isType('array').as('isArray'), + field('str').isType('int64').as('isStrNum'), + ) + .limit(1) + .execute(); + + expectResults(result, { + isInt64: true, + isInt64IsNumber: true, + isInt64IsDecimal128: false, + isFloat64: true, + isFloat64IsNumber: true, + isFloat64IsDecimal128: false, + isStr: true, + isNumStr: false, + isBool: true, + isNull: true, + isGeoPoint: true, + isTimestamp: true, + isBytes: true, + isDocRef: true, + isVector: true, + isMap: true, + isArray: true, + isStrNum: false, + }); + }); + // TODO(new-expression): Add new expression tests above this line }); diff --git a/types/firestore.d.ts b/types/firestore.d.ts index e53d75f7c..921282769 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -5018,6 +5018,23 @@ declare namespace FirebaseFirestore { */ type(): FunctionExpression; + /** + * @beta + * Creates an expression that checks if the result of this expression is of the given type. + * + * @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data. + * + * @example + * ```typescript + * // Check if the 'price' field is specifically an integer (not just 'number') + * field('price').isType('int64'); + * ``` + * + * @param type - The type to check for. + * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. + */ + isType(type: Type): BooleanExpression; + // TODO(new-expression): Add new expression method declarations above this line /** * @beta @@ -9764,6 +9781,39 @@ declare namespace FirebaseFirestore { timezone?: string | Expression, ): FunctionExpression; + /** + * @beta + * + * An enumeration of the different types generated by the Firestore backend. + * + * + */ + export type Type = + | 'null' + | 'array' + | 'boolean' + | 'bytes' + | 'timestamp' + | 'geo_point' + | 'number' + | 'int32' + | 'int64' + | 'float64' + | 'decimal128' + | 'map' + | 'reference' + | 'string' + | 'vector' + | 'max_key' + | 'min_key' + | 'object_id' + | 'regex' + | 'request_timestamp'; + /** * @beta * Creates an expression that returns the data type of the data in the specified field. @@ -9791,6 +9841,44 @@ declare namespace FirebaseFirestore { */ export function type(expression: Expression): FunctionExpression; + /** + * @beta + * Creates an expression that checks if the value in the specified field is of the given type. + * + * @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data. + * + * @example + * ```typescript + * // Check if the 'price' field is a floating point number (evaluating to true inside pipeline conditionals) + * isType('price', 'float64'); + * ``` + * + * @param fieldName - The name of the field to check. + * @param type - The type to check for. + * @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise. + */ + export function isType(fieldName: string, type: Type): BooleanExpression; + /** + * @beta + * Creates an expression that checks if the result of an expression is of the given type. + * + * @remarks Null or undefined fields evaluate to skip/error. Use `ifAbsent()` / `isAbsent()` to evaluate missing data. + * + * @example + * ```typescript + * // Check if the result of a calculation is a number + * isType(add('count', 1), 'number') + * ``` + * + * @param expression - The expression to check. + * @param type - The type to check for. + * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. + */ + export function isType( + expression: Expression, + type: Type, + ): BooleanExpression; + // TODO(new-expression): Add new top-level expression function declarations above this line /** * @beta From 4591b5744dc884810132e69b192cfbf5c9892b18 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 24 Feb 2026 13:18:06 -0500 Subject: [PATCH 2/2] fix docs --- dev/src/pipelines/expression.ts | 10 +++++----- types/firestore.d.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev/src/pipelines/expression.ts b/dev/src/pipelines/expression.ts index fb3fbea01..8f7bb1b9b 100644 --- a/dev/src/pipelines/expression.ts +++ b/dev/src/pipelines/expression.ts @@ -2394,7 +2394,7 @@ export abstract class Expression * field('price').isType('int64'); * ``` * - * @param type - The type to check for. + * @param type The type to check for. * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. */ isType(type: Type): BooleanExpression { @@ -8088,8 +8088,8 @@ export function type( * isType('price', 'float64'); * ``` * - * @param fieldName - The name of the field to check. - * @param type - The type to check for. + * @param fieldName The name of the field to check. + * @param type The type to check for. * @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise. */ export function isType(fieldName: string, type: Type): BooleanExpression; @@ -8106,8 +8106,8 @@ export function isType(fieldName: string, type: Type): BooleanExpression; * isType(add('count', 1), 'number') * ``` * - * @param expression - The expression to check. - * @param type - The type to check for. + * @param expression The expression to check. + * @param type The type to check for. * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. */ export function isType(expression: Expression, type: Type): BooleanExpression; diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 921282769..994b94935 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -5030,7 +5030,7 @@ declare namespace FirebaseFirestore { * field('price').isType('int64'); * ``` * - * @param type - The type to check for. + * @param type The type to check for. * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. */ isType(type: Type): BooleanExpression; @@ -9853,8 +9853,8 @@ declare namespace FirebaseFirestore { * isType('price', 'float64'); * ``` * - * @param fieldName - The name of the field to check. - * @param type - The type to check for. + * @param fieldName The name of the field to check. + * @param type The type to check for. * @returns A new `BooleanExpression` that evaluates to true if the field's value is of the given type, false otherwise. */ export function isType(fieldName: string, type: Type): BooleanExpression; @@ -9870,8 +9870,8 @@ declare namespace FirebaseFirestore { * isType(add('count', 1), 'number') * ``` * - * @param expression - The expression to check. - * @param type - The type to check for. + * @param expression The expression to check. + * @param type The type to check for. * @returns A new `BooleanExpression` that evaluates to true if the expression's result is of the given type, false otherwise. */ export function isType(