diff --git a/.changeset/fix-coalesce-type.md b/.changeset/fix-coalesce-type.md new file mode 100644 index 000000000..f52b10f2d --- /dev/null +++ b/.changeset/fix-coalesce-type.md @@ -0,0 +1,7 @@ +--- +'@tanstack/db': patch +--- + +fix(db): preserve null in coalesce() return type when no guaranteed non-null arg is present + +`coalesce()` was typed as returning `BasicExpression`, losing all type information. The signature now infers types from all arguments via tuple generics, returns the union of non-null arg types, and only removes nullability when at least one argument is statically guaranteed non-null. diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index 887c1468d..49f9560dd 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -285,11 +285,37 @@ export function concat( ) } -export function coalesce(...args: Array): BasicExpression { +// Helper type for coalesce: extracts non-nullish value types from all args +type CoalesceArgTypes> = { + [K in keyof T]: NonNullable> +}[number] + +// Whether any arg in the tuple is statically guaranteed non-null (i.e., does not include null | undefined) +type HasGuaranteedNonNull> = { + [K in keyof T]: null extends ExtractType + ? undefined extends ExtractType + ? false + : false + : undefined extends ExtractType + ? false + : true +}[number] extends false + ? false + : true + +// coalesce() return type: union of all non-null arg types; null included unless a guaranteed non-null arg exists +type CoalesceReturnType> = + HasGuaranteedNonNull extends true + ? BasicExpression> + : BasicExpression | null> + +export function coalesce]>( + ...args: T +): CoalesceReturnType { return new Func( `coalesce`, args.map((arg) => toExpression(arg)), - ) + ) as CoalesceReturnType } export function add( diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index 0e901b7fb..b1565ea32 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -150,7 +150,7 @@ describe(`Query Builder Callback Types`, () => { BasicExpression >() expectTypeOf(coalesce(user.name, `Unknown`)).toEqualTypeOf< - BasicExpression + BasicExpression >() return { diff --git a/packages/db/tests/query/builder/functions.test.ts b/packages/db/tests/query/builder/functions.test.ts index fb6cb4f35..abc6391c9 100644 --- a/packages/db/tests/query/builder/functions.test.ts +++ b/packages/db/tests/query/builder/functions.test.ts @@ -196,7 +196,7 @@ describe(`QueryBuilder Functions`, () => { .from({ employees: employeesCollection }) .select(({ employees }) => ({ id: employees.id, - name_or_default: coalesce([employees.name, `Unknown`]), + name_or_default: coalesce(employees.name, `Unknown`), })) const builtQuery = getQueryIR(query)