Skip to content
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
7 changes: 7 additions & 0 deletions .changeset/fix-coalesce-type.md
Original file line number Diff line number Diff line change
@@ -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<any>`, 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.
30 changes: 28 additions & 2 deletions packages/db/src/query/builder/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,37 @@ export function concat(
)
}

export function coalesce(...args: Array<ExpressionLike>): BasicExpression<any> {
// Helper type for coalesce: extracts non-nullish value types from all args
type CoalesceArgTypes<T extends Array<ExpressionLike>> = {
[K in keyof T]: NonNullable<ExtractType<T[K]>>
}[number]

// Whether any arg in the tuple is statically guaranteed non-null (i.e., does not include null | undefined)
type HasGuaranteedNonNull<T extends Array<ExpressionLike>> = {
[K in keyof T]: null extends ExtractType<T[K]>
? undefined extends ExtractType<T[K]>
? false
: false
: undefined extends ExtractType<T[K]>
? 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<T extends Array<ExpressionLike>> =
HasGuaranteedNonNull<T> extends true
? BasicExpression<CoalesceArgTypes<T>>
: BasicExpression<CoalesceArgTypes<T> | null>

export function coalesce<T extends [ExpressionLike, ...Array<ExpressionLike>]>(
...args: T
): CoalesceReturnType<T> {
return new Func(
`coalesce`,
args.map((arg) => toExpression(arg)),
)
) as CoalesceReturnType<T>
}

export function add<T1 extends ExpressionLike, T2 extends ExpressionLike>(
Expand Down
2 changes: 1 addition & 1 deletion packages/db/tests/query/builder/callback-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe(`Query Builder Callback Types`, () => {
BasicExpression<number>
>()
expectTypeOf(coalesce(user.name, `Unknown`)).toEqualTypeOf<
BasicExpression<any>
BasicExpression<string>
>()

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/db/tests/query/builder/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading