Skip to content

Commit

Permalink
fix: allow interface in sql typearg (#284)
Browse files Browse the repository at this point in the history
* Document recommended sql<> usage
* Add test cases using `interface` as well as `type`
* Remove some more generic constraints
* Generate readme
* alias column
  • Loading branch information
mmkal committed Jun 27, 2021
1 parent 6d011f1 commit ba4a8dd
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 5 deletions.
27 changes: 27 additions & 0 deletions .README/TYPES.md
Expand Up @@ -31,3 +31,30 @@ export default async (
};

```

The `sql` tag itself can receive a generic type, allowing strong type-checking for query results:

```ts
interface Country {
id: number
code: string
}

const countryQuery = sql<Country>`SELECT id, code FROM country`;

const country = await connection.one(countryQuery);

console.log(country.cod) // ts error: Property 'cod' does not exist on type 'Country'. Did you mean 'code'?
```

It is recommended to give a generic type to the `sql` tag itself, rather than the query method, since each query method uses generic types slightly differently:

```ts
// bad
await pool.query<{ foo: string }>(sql`SELECT foo FROM bar`)

// good
await pool.query(sql<{ foo: string }>`SELECT foo FROM bar`)
```

[@slonik/typegen](https://npmjs.com/package/@slonik/typegen) is a community library which will scan your source code for `sql` queries, and apply typescript interfaces to them automatically.
26 changes: 26 additions & 0 deletions README.md
Expand Up @@ -2175,6 +2175,32 @@ export default async (

```
The `sql` tag itself can receive a generic type, allowing strong type-checking for query results:
```ts
interface Country {
id: number
code: string
}

const countryQuery = sql<Country>`SELECT id, code FROM country`;

const country = await connection.one(countryQuery);

console.log(country.cod) // ts error: Property 'cod' does not exist on type 'Country'. Did you mean 'code'?
```
It is recommended to give a generic type to the `sql` tag itself, rather than the query method, since each query method uses generic types slightly differently:
```ts
// bad
await pool.query<{ foo: string }>(sql`SELECT foo FROM bar`)

// good
await pool.query(sql<{ foo: string }>`SELECT foo FROM bar`)
```
[@slonik/typegen](https://npmjs.com/package/@slonik/typegen) is a community library which will scan your source code for `sql` queries, and apply typescript interfaces to them automatically.
<a name="slonik-debugging"></a>
## Debugging
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Expand Up @@ -359,7 +359,7 @@ export type InternalNestedTransactionFunctionType = <T>(
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-unused-vars
export interface TaggedTemplateLiteralInvocationType<Result extends UserQueryResultRowType = QueryResultRowType> extends SqlSqlTokenType { }

export type QueryAnyFirstFunctionType = <T, Row extends Record<string, T> = Record<string, T>>(
export type QueryAnyFirstFunctionType = <T, Row = Record<string, T>>(
sql: TaggedTemplateLiteralInvocationType<Row>,
values?: PrimitiveValueExpressionType[],
) => Promise<ReadonlyArray<Row[keyof Row]>>;
Expand All @@ -375,23 +375,23 @@ export type QueryFunctionType = <T>(
sql: TaggedTemplateLiteralInvocationType<T>,
values?: PrimitiveValueExpressionType[],
) => Promise<QueryResultType<T>>;
export type QueryManyFirstFunctionType = <T, Row extends Record<string, T> = Record<string, T>>(
export type QueryManyFirstFunctionType = <T, Row = Record<string, T>>(
sql: TaggedTemplateLiteralInvocationType<Row>,
values?: PrimitiveValueExpressionType[],
) => Promise<ReadonlyArray<Row[keyof Row]>>;
export type QueryManyFunctionType = <T>(
sql: TaggedTemplateLiteralInvocationType<T>,
values?: PrimitiveValueExpressionType[],
) => Promise<readonly T[]>;
export type QueryMaybeOneFirstFunctionType = <T, Row extends Record<string, T> = Record<string, T>>(
export type QueryMaybeOneFirstFunctionType = <T, Row = Record<string, T>>(
sql: TaggedTemplateLiteralInvocationType<Row>,
values?: PrimitiveValueExpressionType[],
) => Promise<Row[keyof Row] | null>;
export type QueryMaybeOneFunctionType = <T>(
sql: TaggedTemplateLiteralInvocationType<T>,
values?: PrimitiveValueExpressionType[],
) => Promise<T | null>;
export type QueryOneFirstFunctionType = <T, Row extends Record<string, T> = Record<string, T>>(
export type QueryOneFirstFunctionType = <T, Row = Record<string, T>>(
sql: TaggedTemplateLiteralInvocationType<Row>,
values?: PrimitiveValueExpressionType[],
) => Promise<Row[keyof Row]>;
Expand Down
20 changes: 19 additions & 1 deletion test/types.ts
Expand Up @@ -133,7 +133,7 @@ export const queryMethods = async (): Promise<void> => {
},
};

const jsonbSql = sql<RowWithJSONB>`select 123`;
const jsonbSql = sql<RowWithJSONB>`select '{"bar": 123}'::jsonb as foo`;

expectTypeOf(await client.query(jsonbSql)).toEqualTypeOf<QueryResultType<RowWithJSONB>>();
expectTypeOf(await client.one(jsonbSql)).toEqualTypeOf<RowWithJSONB>();
Expand All @@ -144,4 +144,22 @@ export const queryMethods = async (): Promise<void> => {
expectTypeOf(await client.maybeOneFirst(jsonbSql)).toEqualTypeOf<{ bar: number, } | null>();
expectTypeOf(await client.manyFirst(jsonbSql)).toEqualTypeOf<ReadonlyArray<{ bar: number, }>>();
expectTypeOf(await client.anyFirst(jsonbSql)).toEqualTypeOf<ReadonlyArray<{ bar: number, }>>();

// `interface` can behave slightly differently from `type` when it comes to type inference
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IRow {
foo: string | null;
}

const nullableSql = sql<IRow>`select 'abc' as foo`;

expectTypeOf(await client.query(nullableSql)).toEqualTypeOf<QueryResultType<IRow>>();
expectTypeOf(await client.one(nullableSql)).toEqualTypeOf<IRow>();
expectTypeOf(await client.maybeOne(nullableSql)).toEqualTypeOf<IRow | null>();
expectTypeOf(await client.any(nullableSql)).toEqualTypeOf<readonly IRow[]>();
expectTypeOf(await client.many(nullableSql)).toEqualTypeOf<readonly IRow[]>();
expectTypeOf(await client.oneFirst(nullableSql)).toEqualTypeOf<string | null>();
expectTypeOf(await client.maybeOneFirst(nullableSql)).toEqualTypeOf<string | null>();
expectTypeOf(await client.manyFirst(nullableSql)).toEqualTypeOf<ReadonlyArray<string | null>>();
expectTypeOf(await client.anyFirst(nullableSql)).toEqualTypeOf<ReadonlyArray<string | null>>();
};

0 comments on commit ba4a8dd

Please sign in to comment.