Skip to content

Commit f82d679

Browse files
authored
Better sql`` types (porsager#84)
* Improve rows typings * Drop non top level array support Only `YourType[]` accepted * Add TypeScript in README
1 parent 0b33b71 commit f82d679

File tree

2 files changed

+62
-10
lines changed

2 files changed

+62
-10
lines changed

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,46 @@ const [new_user] = await sql`
120120
// new_user = { user_id: 1, name: 'Murray', age: 68 }
121121
```
122122

123+
124+
#### TypeScript support
125+
126+
`postgres` has TypeScript support. You can pass a row list type for your queries in this way:
127+
```ts
128+
interface User {
129+
id: number
130+
name: string
131+
}
132+
133+
const users = await sql<User[]>`SELECT * FROM users`
134+
users[0].id // ok => number
135+
users[1].name // ok => string
136+
users[0].invalid // fails: `invalid` does not exists on `User`
137+
```
138+
139+
However, be sure to check the array length to avoid accessing properties of `undefined` rows:
140+
```ts
141+
const users = await sql<User[]>`SELECT * FROM users WHERE id = ${id}`
142+
if (!users.length)
143+
throw new Error('Not found')
144+
return users[0]
145+
```
146+
147+
You can also prefer destructuring when you only care about a fixed number of rows.
148+
In this case, we recommand you to prefer using tuples to handle `undefined` properly:
149+
```ts
150+
const [user]: [User?] = await sql`SELECT * FROM users WHERE id = ${id}`
151+
if (!user) // => User | undefined
152+
throw new Error('Not found')
153+
return user // => User
154+
155+
// NOTE:
156+
const [first, second]: [User?] = await sql`SELECT * FROM users WHERE id = ${id}` // fails: `second` does not exist on `[User?]`
157+
// vs
158+
const [first, second] = await sql<[User?]>`SELECT * FROM users WHERE id = ${id}` // ok but should fail
159+
```
160+
161+
All the public API is typed. Also, TypeScript support is still in beta. Feel free to open an issue if you have trouble with types.
162+
123163
#### Query parameters
124164

125165
Parameters are automatically inferred and handled by Postgres so that SQL injection isn't possible. No special handling is necessary, simply use JS tagged template literals as usual.

types/index.d.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,18 @@ declare namespace postgres {
247247
[column: string]: any;
248248
}
249249

250+
interface UnlabeledRow<T = any> {
251+
'?column?': T;
252+
}
253+
254+
type MaybeRow = Row | undefined;
255+
256+
type TransformRow<T> = T extends Serializable
257+
? { '?column?': T; }
258+
: T;
259+
260+
type AsRowList<T extends any[]> = { [k in keyof T]: TransformRow<T[k]> };
261+
250262
interface Column<T extends string> {
251263
name: T;
252264
type: number;
@@ -272,13 +284,13 @@ declare namespace postgres {
272284
}
273285

274286
type ExecutionResult<T> = [] & ResultQueryMeta<number, T>;
275-
type RowList<T extends readonly Row[]> = T & ResultQueryMeta<T['length'], keyof T[number]>;
287+
type RowList<T extends MaybeRow[]> = T & Iterable<NonNullable<T[number]>> & ResultQueryMeta<T['length'], keyof T[number]>;
276288

277-
interface PendingQuery<TRow extends readonly Row[]> extends Promise<RowList<TRow>> {
278-
stream(cb: (row: TRow[number], result: ExecutionResult<TRow[number]>) => void): Promise<ExecutionResult<keyof TRow[number]>>;
279-
cursor(cb: (row: TRow[number]) => void): Promise<ExecutionResult<keyof TRow[number]>>;
280-
cursor(size: 1, cb: (row: TRow[number]) => void): Promise<ExecutionResult<keyof TRow[number]>>;
281-
cursor(size: number, cb: (rows: TRow) => void): Promise<ExecutionResult<keyof TRow[number]>>;
289+
interface PendingQuery<TRow extends MaybeRow[]> extends Promise<RowList<TRow>> {
290+
stream(cb: (row: NonNullable<TRow[number]>, result: ExecutionResult<NonNullable<TRow[number]>>) => void): Promise<ExecutionResult<keyof NonNullable<TRow[number]>>>;
291+
cursor(cb: (row: NonNullable<TRow[number]>) => void): Promise<ExecutionResult<keyof NonNullable<TRow[number]>>>;
292+
cursor(size: 1, cb: (row: NonNullable<TRow[number]>) => void): Promise<ExecutionResult<keyof NonNullable<TRow[number]>>>;
293+
cursor(size: number, cb: (rows: NonNullable<TRow[number]>[]) => void): Promise<ExecutionResult<keyof NonNullable<TRow[number]>>>;
282294
}
283295

284296
interface PendingRequest extends Promise<[] & ResultMeta<null>> { }
@@ -296,7 +308,7 @@ declare namespace postgres {
296308
* @param args Interpoled values of the template string
297309
* @returns A promise resolving to the result of your query
298310
*/
299-
<T extends Row | Row[] = Row>(template: TemplateStringsArray, ...args: SerializableParameter[]): PendingQuery<T extends Row[] ? T : T[]>;
311+
<T extends any[] = Row[]>(template: TemplateStringsArray, ...args: SerializableParameter[]): PendingQuery<AsRowList<T>>;
300312

301313
/**
302314
* Escape column names
@@ -321,8 +333,8 @@ declare namespace postgres {
321333
begin<T>(cb: (sql: TransactionSql<TTypes>) => T | Promise<T>): Promise<UnwrapPromiseArray<T>>;
322334
begin<T>(options: string, cb: (sql: TransactionSql<TTypes>) => T | Promise<T>): Promise<UnwrapPromiseArray<T>>;
323335
end(options?: { timeout?: number }): Promise<void>;
324-
file<T extends Row | Row[] = Row>(path: string, options?: { cache?: boolean }): PendingQuery<T extends Row[] ? T : T[]>;
325-
file<T extends Row | Row[] = Row>(path: string, args: SerializableParameter[], options?: { cache?: boolean }): PendingQuery<T extends Row[] ? T : T[]>;
336+
file<T extends any[] = Row[]>(path: string, options?: { cache?: boolean }): PendingQuery<AsRowList<T>>;
337+
file<T extends any[] = Row[]>(path: string, args: SerializableParameter[], options?: { cache?: boolean }): PendingQuery<AsRowList<T>>;
326338
json(value: any): Parameter;
327339
listen(channel: string, cb: (value?: string) => void): PendingRequest;
328340
notify(channel: string, payload: string): PendingRequest;
@@ -333,7 +345,7 @@ declare namespace postgres {
333345
? (...args: Parameters<TTypes[name]>) => postgres.Parameter<ReturnType<TTypes[name]>>
334346
: (...args: any) => postgres.Parameter<any>;
335347
};
336-
unsafe<T extends Row | Row[] = any[]>(query: string, parameters?: SerializableParameter[]): PendingQuery<T extends Row[] ? T : T[]>;
348+
unsafe<T extends any[] = Row[]>(query: string, parameters?: SerializableParameter[]): PendingQuery<AsRowList<T>>;
337349
}
338350

339351
interface TransactionSql<TTypes extends JSToPostgresTypeMap> extends Sql<TTypes> {

0 commit comments

Comments
 (0)