fix: implement strict type coercion in DataContext#861
fix: implement strict type coercion in DataContext#861JasonOA888 wants to merge 2 commits intogoogle:mainfrom
Conversation
- Add coercion.ts utility with coerceToString, coerceToNumber, coerceToBoolean - Update DataContext to import coercion utilities - Prevents bugs like [object Object] in text labels Fixes google#846
There was a problem hiding this comment.
Code Review
This pull request introduces new strict type coercion utilities, which is a valuable step towards standardizing data handling. However, there are a few issues to address. The most significant is a logic error in coerceToBoolean that causes it to incorrectly evaluate most non-empty strings to true. Additionally, the new coercion.ts file is missing unit tests, which is required by the repository's contribution guidelines. Lastly, coerceToString can still produce [object Object] in edge cases, which contradicts the documentation.
| return false; | ||
| } | ||
| if (typeof value === "string") { | ||
| return value.toLowerCase() === "true" || value !== ""; |
There was a problem hiding this comment.
The current logic for coercing a string to a boolean, value.toLowerCase() === "true" || value !== "", will evaluate to true for any non-empty string (e.g., "false" becomes true). This is because for any non-empty string, the value !== "" part of the condition is always true, making the entire expression true.
Given the function's goal of strict coercion and the JSDoc rule "true" (case-insensitive) → true, a stricter implementation is needed. To fix this, the logic should only check if the string is a case-insensitive match for "true".
| return value.toLowerCase() === "true" || value !== ""; | |
| return value.toLowerCase() === "true"; |
| /* | ||
| * Copyright 2025 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| /** | ||
| * Strict Type Coercion Utilities | ||
| * | ||
| * Implements the A2UI protocol's standard coercion rules to ensure | ||
| * consistent handling of null/undefined values and type conversions. | ||
| * | ||
| * Without central enforcement, component authors must manually handle | ||
| * these edge cases, leading to bugs like [object Object] appearing | ||
| * in text labels. | ||
| */ | ||
|
|
||
| /** | ||
| * Coerces any value to a string following A2UI protocol rules: | ||
| * - null/undefined → "" | ||
| * - objects → localized string representation (not "[object Object]") | ||
| * - other types → String(value) | ||
| */ | ||
| export function coerceToString(value: unknown): string { | ||
| if (value === null || value === undefined) { | ||
| return ""; | ||
| } | ||
| if (typeof value === "object") { | ||
| // Avoid "[object Object]" by using JSON.stringify for plain objects | ||
| // or calling toString for objects with custom implementations | ||
| if (Array.isArray(value)) { | ||
| return value.map(coerceToString).join(", "); | ||
| } | ||
| if (value instanceof Error) { | ||
| return value.message; | ||
| } | ||
| try { | ||
| return JSON.stringify(value); | ||
| } catch { | ||
| return String(value); | ||
| } | ||
| } | ||
| return String(value); | ||
| } | ||
|
|
||
| /** | ||
| * Coerces any value to a number following A2UI protocol rules: | ||
| * - null/undefined → 0 | ||
| * - strings → parsed number (NaN becomes 0) | ||
| * - booleans → 1 for true, 0 for false | ||
| * - other types → Number(value) | ||
| */ | ||
| export function coerceToNumber(value: unknown): number { | ||
| if (value === null || value === undefined) { | ||
| return 0; | ||
| } | ||
| if (typeof value === "string") { | ||
| const parsed = parseFloat(value); | ||
| return isNaN(parsed) ? 0 : parsed; | ||
| } | ||
| if (typeof value === "boolean") { | ||
| return value ? 1 : 0; | ||
| } | ||
| const result = Number(value); | ||
| return isNaN(result) ? 0 : result; | ||
| } | ||
|
|
||
| /** | ||
| * Coerces any value to a boolean following A2UI protocol rules: | ||
| * - "true" (case-insensitive) → true | ||
| * - non-zero numbers → true | ||
| * - null/undefined → false | ||
| * - other types → Boolean(value) | ||
| */ | ||
| export function coerceToBoolean(value: unknown): boolean { | ||
| if (value === null || value === undefined) { | ||
| return false; | ||
| } | ||
| if (typeof value === "string") { | ||
| return value.toLowerCase() === "true" || value !== ""; | ||
| } | ||
| if (typeof value === "number") { | ||
| return value !== 0; | ||
| } | ||
| return Boolean(value); | ||
| } | ||
|
|
||
| /** | ||
| * Coerces a value to a specific target type. | ||
| */ | ||
| export function coerceValue<T>(value: unknown, targetType: "string"): string; | ||
| export function coerceValue<T>(value: unknown, targetType: "number"): number; | ||
| export function coerceValue<T>(value: unknown, targetType: "boolean"): boolean; | ||
| export function coerceValue<T>(value: unknown, targetType: string): unknown { | ||
| switch (targetType) { | ||
| case "string": | ||
| return coerceToString(value); | ||
| case "number": | ||
| return coerceToNumber(value); | ||
| case "boolean": | ||
| return coerceToBoolean(value); | ||
| default: | ||
| return value; | ||
| } | ||
| } |
There was a problem hiding this comment.
This new file introduces important coercion logic but lacks unit tests to verify its correctness and prevent future regressions. The repository style guide requires tests for new code.
Please add a test file (e.g., coercion.test.ts) with unit tests for the new coercion functions. The tests should cover the cases mentioned in the PR description as well as other edge cases, such as:
coerceToString:null,undefined, objects with circular references.coerceToBoolean:"false", empty string, and other non-"true"strings.coerceToNumber:null,undefined, invalid strings.
References
- If there are code changes, code should have tests. (link)
| try { | ||
| return JSON.stringify(value); | ||
| } catch { | ||
| return String(value); | ||
| } |
There was a problem hiding this comment.
The try...catch block for JSON.stringify is a good way to handle potential errors, but the fallback to String(value) can still result in "[object Object]" for objects with circular references. This contradicts the function's documentation and the PR's goal of eliminating "[object Object]".
To make the behavior clearer, you could update the comment to acknowledge this edge case. If avoiding "[object Object]" is a strict requirement, you might need to consider a more robust serialization approach that can handle circular references, though that might be out of scope for this change. For now, clarifying the comment would be a good improvement.
- Fix coerceToBoolean: only 'true' (case-insensitive) → true - Fix coerceToString: handle objects with custom toString (e.g., Date) - Add comprehensive unit tests for all coercion functions Co-authored-by: Gemini Code Assist <gemini-code-assist@google.com>
Summary
Fixes #846 - Implement strict type coercion rules in DataContext.
Problem
The A2UI protocol defines standard coercion rules (e.g.
nullresolving to""for strings). Without central enforcement, component authors must manually handle these edge cases, leading to bugs like[object Object]appearing in text labels.Solution
1. New Coercion Utilities (
coercion.ts)coerceToStringnull/undefined→"", objects → JSON (not[object Object])coerceToNumbernull/undefined→0, strings → parsed (NaN → 0)coerceToBoolean2. Updated DataContext
resolveDynamicValueandresolveSignalTesting
Files Changed
renderers/web_core/src/v0_9/rendering/coercion.ts(new)renderers/web_core/src/v0_9/rendering/data-context.ts(modified)