Skip to content

Commit

Permalink
add assertType method
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Dec 27, 2022
1 parent 13355e7 commit 71419b5
Show file tree
Hide file tree
Showing 12 changed files with 749 additions and 291 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion recipes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
* [Deduplicate joins](https://github.com/koskimas/kysely/tree/master/recipes/deduplicate-joins.md)
* [Extending kysely](https://github.com/koskimas/kysely/tree/master/recipes/extending-kysely.md)
* [Raw SQL](https://github.com/koskimas/kysely/tree/master/recipes/raw-sql.md)
* [Schemas](https://github.com/koskimas/kysely/tree/master/recipes/schemas.md)
* [Schemas](https://github.com/koskimas/kysely/tree/master/recipes/schemas.md)
* [Dealing with the `Type instantiation is excessively deep and possibly infinite` error](https://github.com/koskimas/kysely/tree/master/recipes/excessively-deep-types.md)
47 changes: 47 additions & 0 deletions recipes/excessively-deep-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Dealing with the `Type instantiation is excessively deep and possibly infinite` error

Kysely uses complex type magic to achieve its type safety. This complexity is sometimes too much for typescript and you get errors like this:

```
error TS2589: Type instantiation is excessively deep and possibly infinite.
```

In these case you can often use the [assertType](https://koskimas.github.io/kysely/classes/SelectQueryBuilder.html#assertType) method to help typescript a little bit. When you use this method to assert the output type of a query, Kysely can drop the complex output type that consists of multiple nested helper types and replace it with the simple asserted type.

Using this method doesn't reduce type safety at all. You have to pass in a type that is structurally equal to the current type.

For example having more than six `with` statements in a query can lead to the `TS2589` error:

```ts
const res = await db
.with('w1', (qb) => qb.selectFrom('person').select('first_name as fn1'))
.with('w2', (qb) => qb.selectFrom('person').select('first_name as fn2'))
.with('w3', (qb) => qb.selectFrom('person').select('first_name as fn3'))
.with('w4', (qb) => qb.selectFrom('person').select('first_name as fn4'))
.with('w5', (qb) => qb.selectFrom('person').select('first_name as fn5'))
.with('w6', (qb) => qb.selectFrom('person').select('first_name as fn6'))
.with('w7', (qb) => qb.selectFrom('person').select('first_name as fn7'))
.selectFrom(['w1', 'w2', 'w3', 'w4', 'w5', 'w6', 'w7'])
.selectAll()
.executeTakeFirstOrThrow()
```

But if you simplify one or more of the `with` statements using `assertType`, you get rid of the error:

```ts
const res = await db
.with('w1', (qb) => qb.selectFrom('person').select('first_name as fn1'))
.with('w2', (qb) => qb.selectFrom('person').select('first_name as fn2'))
.with('w3', (qb) => qb.selectFrom('person').select('first_name as fn3'))
.with('w4', (qb) => qb.selectFrom('person').select('first_name as fn4'))
.with('w5', (qb) => qb.selectFrom('person').select('first_name as fn5'))
.with('w6', (qb) => qb.selectFrom('person').select('first_name as fn6'))
.with('w7', (qb) => qb.selectFrom('person').select('first_name as fn7').assertType<{ fn7: string }>())
.selectFrom(['w1', 'w2', 'w3', 'w4', 'w5', 'w6', 'w7'])
.selectAll()
.executeTakeFirstOrThrow()
```

The type you provide for `assertType` must be structurally equal to the return type of the subquery. Therefore no type safety is lost.

I know what you're thinking: can't this be done automatically? No, unfortunately it can't. There's no way to do this using current typescript features. Typescript drags along all the parts the type is built with. Even though it could simplify the type into a simple object, it doesn't. We need to explictly tell it to do that using the `assertType` method.
48 changes: 48 additions & 0 deletions src/query-builder/delete-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
parseExists,
parseNotExists,
} from '../parser/unary-operation-parser.js'
import { KyselyTypeError } from '../util/type-error.js'

export class DeleteQueryBuilder<DB, TB extends keyof DB, O>
implements
Expand Down Expand Up @@ -568,6 +569,53 @@ export class DeleteQueryBuilder<DB, TB extends keyof DB, O>
return new DeleteQueryBuilder(this.#props)
}

/**
* Asserts that query's output row type equals the given type `T`.
*
* This method can be used to simplify excessively complex types to make typescript happy
* and much faster.
*
* Kysely uses complex type magic to achieve its type safety. This complexity is sometimes too much
* for typescript and you get errors like this:
*
* ```
* error TS2589: Type instantiation is excessively deep and possibly infinite.
* ```
*
* In these case you can often use this method to help typescript a little bit. When you use this
* method to assert the output type of a query, Kysely can drop the complex output type that
* consists of multiple nested helper types and replace it with the simple asserted type.
*
* Using this method doesn't reduce type safety at all. You have to pass in a type that is
* structurally equal to the current type.
*
* ### Examples
*
* ```ts
* const result = await db
* .with('deleted_person', (qb) => qb
* .deleteFrom('person')
* .where('id', '=', person.id)
* .returning('first_name')
* .assertType<{ first_name: string }>()
* )
* .with('deleted_pet', (qb) => qb
* .deleteFrom('pet')
* .where('owner_id', '=', person.id)
* .returning(['name as pet_name', 'species'])
* .assertType<{ pet_name: string, species: Species }>()
* )
* .selectFrom(['deleted_person', 'deleted_pet'])
* .selectAll()
* .executeTakeFirstOrThrow()
* ```
*/
assertType<T extends O>(): O extends T
? DeleteQueryBuilder<DB, TB, T>
: KyselyTypeError<`assertType() call failed: The type passed in is not equal to the output type of the query.`> {
return new DeleteQueryBuilder(this.#props) as unknown as any
}

/**
* Returns a copy of this DeleteQueryBuilder instance with the given plugin installed.
*/
Expand Down
48 changes: 48 additions & 0 deletions src/query-builder/insert-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { Selectable } from '../util/column-type.js'
import { Explainable, ExplainFormat } from '../util/explainable.js'
import { ExplainNode } from '../operation-node/explain-node.js'
import { Expression } from '../expression/expression.js'
import { KyselyTypeError } from '../util/type-error.js'

export class InsertQueryBuilder<DB, TB extends keyof DB, O>
implements
Expand Down Expand Up @@ -612,6 +613,53 @@ export class InsertQueryBuilder<DB, TB extends keyof DB, O>
return new InsertQueryBuilder(this.#props)
}

/**
* Asserts that query's output row type equals the given type `T`.
*
* This method can be used to simplify excessively complex types to make typescript happy
* and much faster.
*
* Kysely uses complex type magic to achieve its type safety. This complexity is sometimes too much
* for typescript and you get errors like this:
*
* ```
* error TS2589: Type instantiation is excessively deep and possibly infinite.
* ```
*
* In these case you can often use this method to help typescript a little bit. When you use this
* method to assert the output type of a query, Kysely can drop the complex output type that
* consists of multiple nested helper types and replace it with the simple asserted type.
*
* Using this method doesn't reduce type safety at all. You have to pass in a type that is
* structurally equal to the current type.
*
* ### Examples
*
* ```ts
* const result = await db
* .with('new_person', (qb) => qb
* .insertInto('person')
* .values(person)
* .returning('id')
* .assertType<{ id: string }>()
* )
* .with('new_pet', (qb) => qb
* .insertInto('pet')
* .values({ owner_id: (eb) => eb.selectFrom('new_person').select('id'), ...pet })
* .returning(['name as pet_name', 'species'])
* .assertType<{ pet_name: string, species: Species }>()
* )
* .selectFrom(['new_person', 'new_pet'])
* .selectAll()
* .executeTakeFirstOrThrow()
* ```
*/
assertType<T extends O>(): O extends T
? InsertQueryBuilder<DB, TB, T>
: KyselyTypeError<`assertType() call failed: The type passed in is not equal to the output type of the query.`> {
return new InsertQueryBuilder(this.#props) as unknown as any
}

/**
* Returns a copy of this InsertQueryBuilder instance with the given plugin installed.
*/
Expand Down
46 changes: 46 additions & 0 deletions src/query-builder/select-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
parseExists,
parseNotExists,
} from '../parser/unary-operation-parser.js'
import { KyselyTypeError } from '../util/type-error.js'

export class SelectQueryBuilder<DB, TB extends keyof DB, O>
implements
Expand Down Expand Up @@ -1586,6 +1587,51 @@ export class SelectQueryBuilder<DB, TB extends keyof DB, O>
return new SelectQueryBuilder(this.#props)
}

/**
* Asserts that query's output row type equals the given type `T`.
*
* This method can be used to simplify excessively complex types to make typescript happy
* and much faster.
*
* Kysely uses complex type magic to achieve its type safety. This complexity is sometimes too much
* for typescript and you get errors like this:
*
* ```
* error TS2589: Type instantiation is excessively deep and possibly infinite.
* ```
*
* In these case you can often use this method to help typescript a little bit. When you use this
* method to assert the output type of a query, Kysely can drop the complex output type that
* consists of multiple nested helper types and replace it with the simple asserted type.
*
* Using this method doesn't reduce type safety at all. You have to pass in a type that is
* structurally equal to the current type.
*
* ### Examples
*
* ```ts
* const result = await db
* .with('first_and_last', (qb) => qb
* .selectFrom('person')
* .select(['first_name', 'last_name'])
* .assertType<{ first_name: string, last_name: string }>()
* )
* .with('age', (qb) => qb
* .selectFrom('person')
* .select('age')
* .assertType<{ age: number }>()
* )
* .selectFrom(['first_and_last', 'age'])
* .selectAll()
* .executeTakeFirstOrThrow()
* ```
*/
assertType<T extends O>(): O extends T
? SelectQueryBuilder<DB, TB, T>
: KyselyTypeError<`assertType() call failed: The type passed in is not equal to the output type of the query.`> {
return new SelectQueryBuilder(this.#props) as unknown as any
}

/**
* Returns a copy of this SelectQueryBuilder instance with the given plugin installed.
*/
Expand Down
50 changes: 50 additions & 0 deletions src/query-builder/update-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
parseExists,
parseNotExists,
} from '../parser/unary-operation-parser.js'
import { KyselyTypeError } from '../util/type-error.js'

export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
implements
Expand Down Expand Up @@ -664,6 +665,55 @@ export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
return new UpdateQueryBuilder(this.#props)
}

/**
* Asserts that query's output row type equals the given type `T`.
*
* This method can be used to simplify excessively complex types to make typescript happy
* and much faster.
*
* Kysely uses complex type magic to achieve its type safety. This complexity is sometimes too much
* for typescript and you get errors like this:
*
* ```
* error TS2589: Type instantiation is excessively deep and possibly infinite.
* ```
*
* In these case you can often use this method to help typescript a little bit. When you use this
* method to assert the output type of a query, Kysely can drop the complex output type that
* consists of multiple nested helper types and replace it with the simple asserted type.
*
* Using this method doesn't reduce type safety at all. You have to pass in a type that is
* structurally equal to the current type.
*
* ### Examples
*
* ```ts
* const result = await db
* .with('updated_person', (qb) => qb
* .updateTable('person')
* .set(person)
* .where('id', '=', person.id)
* .returning('first_name')
* .assertType<{ first_name: string }>()
* )
* .with('updated_pet', (qb) => qb
* .updateTable('pet')
* .set(pet)
* .where('owner_id', '=', person.id)
* .returning(['name as pet_name', 'species'])
* .assertType<{ pet_name: string, species: Species }>()
* )
* .selectFrom(['updated_person', 'updated_pet'])
* .selectAll()
* .executeTakeFirstOrThrow()
* ```
*/
assertType<T extends O>(): O extends T
? UpdateQueryBuilder<DB, UT, TB, T>
: KyselyTypeError<`assertType() call failed: The type passed in is not equal to the output type of the query.`> {
return new UpdateQueryBuilder(this.#props) as unknown as any
}

/**
* Returns a copy of this UpdateQueryBuilder instance with the given plugin installed.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/util/type-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface KyselyTypeError<E extends string> {
readonly __kyselyTypeError__: E
}
54 changes: 54 additions & 0 deletions test/typings/test-d/assert-type.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Kysely } from '..'
import { Database } from '../shared'
import { expectType, expectError } from 'tsd'

async function testAssertType(db: Kysely<Database>) {
const r1 = await db
.selectFrom('person')
.select('first_name as fn')
.assertType<{ fn: string }>()
.executeTakeFirstOrThrow()

expectType<{ fn: string }>(r1)

const r2 = await db
.updateTable('person')
.returning('first_name as fn')
.assertType<{ fn: string }>()
.executeTakeFirstOrThrow()

expectType<{ fn: string }>(r2)

const r3 = await db
.insertInto('person')
.values({ first_name: 'foo', age: 54, gender: 'other' })
.returning('first_name as fn')
.assertType<{ fn: string }>()
.executeTakeFirstOrThrow()

expectType<{ fn: string }>(r3)

const r4 = await db
.deleteFrom('person')
.returning('first_name as fn')
.assertType<{ fn: string }>()
.executeTakeFirstOrThrow()

expectType<{ fn: string }>(r4)

expectError(
db
.selectFrom('person')
.select('first_name as fn')
.assertType<{ wrong: string }>()
.execute()
)

expectError(
db
.selectFrom('person')
.select('first_name as fn')
.assertType<{ fn: string; extra: number }>()
.execute()
)
}
Loading

0 comments on commit 71419b5

Please sign in to comment.