Skip to content

Commit 1ef3203

Browse files
feat: support narrowing by deep object keys in NarrowPartial (#1667)
Co-authored-by: Igal Klebanov <igalklebanov@gmail.com>
1 parent 122bbfb commit 1ef3203

7 files changed

Lines changed: 66 additions & 33 deletions

File tree

src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,18 +245,20 @@ export type {
245245
DrainOuterGeneric,
246246
Equals,
247247
ExtractColumnType,
248-
UnknownRow,
249-
Simplify,
250-
SqlBool,
248+
NarrowPartial,
249+
NotNull,
251250
Nullable,
252251
NumbersWhenDataTypeNotAvailable,
253-
NotNull,
254252
NumericString,
255253
ShallowDehydrateObject,
256254
ShallowDehydrateValue,
255+
Simplify,
256+
SimplifyDeep,
257257
SimplifyResult,
258258
SimplifySingleResult,
259+
SqlBool,
259260
StringsWhenDataTypeNotAvailable,
261+
UnknownRow,
260262
} from './util/type-utils.js'
261263
export * from './util/infer-result.js'
262264
export { logOnce } from './util/log-once.js'

src/util/type-utils.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { InsertResult } from '../query-builder/insert-result.js'
21
import type { DeleteResult } from '../query-builder/delete-result.js'
2+
import type { InsertResult } from '../query-builder/insert-result.js'
3+
import type { MergeResult } from '../query-builder/merge-result.js'
34
import type { UpdateResult } from '../query-builder/update-result.js'
45
import type { KyselyTypeError } from './type-error.js'
5-
import type { MergeResult } from '../query-builder/merge-result.js'
66

77
/**
88
* Given a database type and a union of table names in that db, returns
@@ -154,19 +154,25 @@ export type Equals<T, U> =
154154
? true
155155
: false
156156

157-
export type NarrowPartial<O, T> = DrainOuterGeneric<
158-
T extends object
159-
? {
160-
[K in keyof O & string]: K extends keyof T
161-
? T[K] extends NotNull
162-
? Exclude<O[K], null>
157+
export type NarrowPartial<O, T> = T extends object
158+
? DrainOuterGeneric<{
159+
[K in keyof O & string]: K extends keyof T
160+
? T[K] extends NotNull
161+
? Exclude<O[K], null>
162+
: T[K] extends object
163+
? SimplifyDeep<O[K] & NarrowPartial<O[K], T[K]>>
163164
: T[K] extends O[K]
164165
? T[K]
165166
: KyselyTypeError<`$narrowType() call failed: passed type does not exist in '${K}'s type union`>
166-
: O[K]
167-
}
168-
: never
169-
>
167+
: O[K]
168+
}>
169+
: never
170+
171+
export type SimplifyDeep<T> = T extends object
172+
? T extends Date | RegExp | Map<any, any> | Set<any>
173+
? T
174+
: DrainOuterGeneric<{ [K in keyof T]: SimplifyDeep<T[K]> } & {}>
175+
: T
170176

171177
/**
172178
* A type constant for marking a column as not null. Can be used with `$narrowPartial`.

test/composite-ts/index.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function foo<T extends 'myColumn'>(db: Kysely<DB>, field: T) {
1212
.selectFrom('MyTable')
1313
.select(field) // <------- was missing DrainOuterGeneric & ExtractColumnType
1414
.$narrowType<{}>() // <------- was missing KyselyTypeError
15-
.executeTakeFirst() // <------- was missing SimplifySingleResult
15+
.executeTakeFirst() // <------- was missing Simplify & children, and NarrowPartial
1616
}
1717

1818
export function bar<T extends keyof DB['MyTable']>(
@@ -22,7 +22,7 @@ export function bar<T extends keyof DB['MyTable']>(
2222
return db
2323
.insertInto('MyTable')
2424
.values({ myColumn: 'test', anotherColumn: 1 })
25-
.returning(columns) // <----- was missing SimplifyResult
25+
.returning(columns) // <----- was missing Simplify & children, and NarrowPartial
2626
.execute()
2727
}
2828

test/composite-ts/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/ts-benchmarks/with.bench.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ bench('kysely.with(cte, qc => qc.insertInto(table))', () => {
1919

2020
bench('kysely.with(cte, qc => qc.updateTable(table))', () => {
2121
return kysely.with('cte', (qc) => qc.updateTable('my_table').returningAll())
22-
}).types([23993, 'instantiations'])
22+
}).types([23981, 'instantiations'])
2323

2424
bench('kysely.with(cte, qc => qc.deleteFrom(table))', () => {
2525
return kysely.with('cte', (qc) => qc.deleteFrom('my_table').returningAll())
26-
}).types([20729, 'instantiations'])
26+
}).types([20717, 'instantiations'])
2727

2828
bench('kyselyAny.with(cte, qc => qc.selectFrom(table))', () => {
2929
return kyselyAny.with('cte', (qc) => qc.selectFrom('my_table').selectAll())
@@ -37,11 +37,11 @@ bench('kyselyAny.with(cte, qc => qc.updateTable(table))', () => {
3737
return kyselyAny.with('cte', (qc) =>
3838
qc.updateTable('my_table').returningAll(),
3939
)
40-
}).types([23783, 'instantiations'])
40+
}).types([23771, 'instantiations'])
4141

4242
bench('kyselyAny.with(cte, qc => qc.deleteFrom(table))', () => {
4343
return kyselyAny.with('cte', (qc) => qc.deleteFrom('my_table').returningAll())
44-
}).types([20519, 'instantiations'])
44+
}).types([20507, 'instantiations'])
4545

4646
bench('kysely.with(cte, () => selectQuery)', () => {
4747
return kysely.with('cte', () => kysely.selectFrom('my_table').selectAll())
@@ -53,11 +53,11 @@ bench('kysely.with(cte, () => insertQuery)', () => {
5353

5454
bench('kysely.with(cte, () => updateQuery)', () => {
5555
return kysely.with('cte', () => kysely.updateTable('my_table').returningAll())
56-
}).types([23981, 'instantiations'])
56+
}).types([23969, 'instantiations'])
5757

5858
bench('kysely.with(cte, () => deleteQuery)', () => {
5959
return kysely.with('cte', () => kysely.deleteFrom('my_table').returningAll())
60-
}).types([20717, 'instantiations'])
60+
}).types([20705, 'instantiations'])
6161

6262
bench('kyselyAny.with(cte, () => selectQuery)', () => {
6363
return kyselyAny.with('cte', () =>
@@ -75,13 +75,13 @@ bench('kyselyAny.with(cte, () => updateQuery)', () => {
7575
return kyselyAny.with('cte', () =>
7676
kyselyAny.updateTable('my_table').returningAll(),
7777
)
78-
}).types([23771, 'instantiations'])
78+
}).types([23759, 'instantiations'])
7979

8080
bench('kyselyAny.with(cte, () => deleteQuery)', () => {
8181
return kyselyAny.with('cte', () =>
8282
kyselyAny.deleteFrom('my_table').returningAll(),
8383
)
84-
}).types([20507, 'instantiations'])
84+
}).types([20495, 'instantiations'])
8585

8686
bench('kysely.with(cte, selectQuery)', () => {
8787
return kysely.with('cte', kysely.selectFrom('my_table').selectAll())
@@ -93,11 +93,11 @@ bench('kysely.with(cte, insertQuery)', () => {
9393

9494
bench('kysely.with(cte, updateQuery)', () => {
9595
return kysely.with('cte', kysely.updateTable('my_table').returningAll())
96-
}).types([24025, 'instantiations'])
96+
}).types([24013, 'instantiations'])
9797

9898
bench('kysely.with(cte, deleteQuery)', () => {
9999
return kysely.with('cte', kysely.deleteFrom('my_table').returningAll())
100-
}).types([20762, 'instantiations'])
100+
}).types([20750, 'instantiations'])
101101

102102
bench('kyselyAny.with(cte, selectQuery)', () => {
103103
return kyselyAny.with('cte', kyselyAny.selectFrom('my_table').selectAll())
@@ -109,8 +109,8 @@ bench('kyselyAny.with(cte, insertQuery)', () => {
109109

110110
bench('kyselyAny.with(cte, updateQuery)', () => {
111111
return kyselyAny.with('cte', kyselyAny.updateTable('my_table').returningAll())
112-
}).types([23815, 'instantiations'])
112+
}).types([23803, 'instantiations'])
113113

114114
bench('kyselyAny.with(cte, deleteQuery)', () => {
115115
return kyselyAny.with('cte', kyselyAny.deleteFrom('my_table').returningAll())
116-
}).types([20552, 'instantiations'])
116+
}).types([20540, 'instantiations'])

test/typings/shared.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export interface PersonMetadata {
8080
}
8181
tags: string[]
8282
}>
83+
discriminatedUnionProfile: JSONColumnType<{
84+
auth:
85+
| { type: 'token'; token: string }
86+
| { type: 'session'; session_id: string }
87+
tags: string[]
88+
}>
8389
experience: JSONColumnType<
8490
{
8591
establishment: string

test/typings/test-d/select.test-d.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { expectError, expectType } from 'tsd'
12
import {
23
type Expression,
34
type ExpressionWrapper,
@@ -9,7 +10,6 @@ import {
910
sql,
1011
} from '../index.js'
1112
import type { Database, Person } from '../shared.js'
12-
import { expectType, expectError } from 'tsd'
1313

1414
async function testSelectSingle(db: Kysely<Database>) {
1515
const qb = db.selectFrom('person')
@@ -115,6 +115,11 @@ async function testSelectSingle(db: Kysely<Database>) {
115115
.$narrowType<NarrowTarget>()
116116
.execute()
117117

118+
expectType<
119+
| { callback_url: string; queue_id: null }
120+
| { callback_url: null; queue_id: string }
121+
>(r15)
122+
118123
// Narrow not null
119124
const [r16] = await db
120125
.selectFrom('action')
@@ -134,6 +139,20 @@ async function testSelectSingle(db: Kysely<Database>) {
134139
expectType<string>(r17.callback_url)
135140
expectType<string>(r17.queue_id)
136141

142+
// Narrow discriminated union
143+
const [r18] = await db
144+
.selectFrom('person_metadata')
145+
.select(['discriminatedUnionProfile'])
146+
.$narrowType<{ discriminatedUnionProfile: { auth: { type: 'token' } } }>()
147+
.execute()
148+
149+
expectType<{
150+
discriminatedUnionProfile: {
151+
auth: { type: 'token'; token: string }
152+
tags: string[]
153+
}
154+
}>(r18)
155+
137156
const expr1 = db.selectFrom('person').select('first_name').$asScalar()
138157
expectType<ExpressionWrapper<Database, 'person', string>>(expr1)
139158

0 commit comments

Comments
 (0)