diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts index 46c357dfe..ac93b30fc 100644 --- a/src/query-builder/select-query-builder.ts +++ b/src/query-builder/select-query-builder.ts @@ -1558,6 +1558,20 @@ export interface SelectQueryBuilder<DB, TB extends keyof DB, O> * * functionThatExpectsPersonWithNonNullValue(person) * ``` + * + * Giving the explicit narrowed type (`string` in the example above) works fine for + * simple types. If the type is complex, for example a JSON column or a subquery, + * you can use the special `NotNull` type to make the column not null. + * + * ```ts + * const person = await db.selectFrom('person') + * .where('nullable_column', 'is not', null) + * .selectAll() + * .$narrowType<{ nullable_column: NotNull }>() + * .executeTakeFirstOrThrow() + * + * functionThatExpectsPersonWithNonNullValue(person) + * ``` */ $narrowType<T>(): SelectQueryBuilder<DB, TB, NarrowPartial<O, T>> diff --git a/src/util/type-utils.ts b/src/util/type-utils.ts index dbd9f5ec2..b9817a481 100644 --- a/src/util/type-utils.ts +++ b/src/util/type-utils.ts @@ -178,18 +178,35 @@ export type Equals<T, U> = (<G>() => G extends T ? 1 : 2) extends < ? true : false -export type NarrowPartial<S, T> = DrainOuterGeneric< +export type NarrowPartial<O, T> = DrainOuterGeneric< T extends object ? { - [K in keyof S & string]: K extends keyof T - ? T[K] extends S[K] + [K in keyof O & string]: K extends keyof T + ? T[K] extends NotNull + ? Exclude<O[K], null> + : T[K] extends O[K] ? T[K] : KyselyTypeError<`$narrowType() call failed: passed type does not exist in '${K}'s type union`> - : S[K] + : O[K] } : never > +/** + * A type constant for marking a column as not null. Can be used with `$narrowPartial`. + * + * Example: + * + * ```ts + * const person = await db.selectFrom('person') + * .where('nullable_column', 'is not', null) + * .selectAll() + * .$narrowType<{ nullable_column: NotNull }>() + * .executeTakeFirstOrThrow() + * ``` + */ +export type NotNull = { readonly __excludeNull__: true } + export type SqlBool = boolean | 0 | 1 /** diff --git a/test/typings/test-d/select.test-d.ts b/test/typings/test-d/select.test-d.ts index 9216a62c4..3aa4e8feb 100644 --- a/test/typings/test-d/select.test-d.ts +++ b/test/typings/test-d/select.test-d.ts @@ -1,4 +1,12 @@ -import { Expression, Kysely, RawBuilder, Selectable, Simplify, sql } from '..' +import { + Expression, + Kysely, + NotNull, + RawBuilder, + Selectable, + Simplify, + sql, +} from '..' import { Database, Person } from '../shared' import { expectType, expectError } from 'tsd' @@ -106,7 +114,24 @@ async function testSelectSingle(db: Kysely<Database>) { .$narrowType<NarrowTarget>() .execute() - expectType<NarrowTarget>(r15) + // Narrow not null + const [r16] = await db + .selectFrom('action') + .select(['callback_url', 'queue_id']) + .$narrowType<{ callback_url: NotNull }>() + .execute() + + expectType<string>(r16.callback_url) + expectType<string | null>(r16.queue_id) + + const [r17] = await db + .selectFrom('action') + .select(['callback_url', 'queue_id']) + .$narrowType<{ callback_url: NotNull; queue_id: NotNull }>() + .execute() + + expectType<string>(r17.callback_url) + expectType<string>(r17.queue_id) } async function testSelectAll(db: Kysely<Database>) {