I've been experimenting with TypeScript with the hope of making a type-safe database query library (loosely based on Scala's Slick). I've made some good progress with the help of Mapped Types but I'm getting stuck on preserving the underlying type of the Column. See the code below for a comprehensive example:
class Table {}
class TableExpression<TK extends Table> {
table: TK
alias: string
constructor(table: TK, alias?: string) {
this.table = table
this.alias = alias || table.constructor.name
}
}
class Column<T> {
name: string
defaultValue?: T
}
class StringC extends Column<string> {
constructor(name: string, defaultValue?: string) {
super()
this.name = name
this.defaultValue = defaultValue
}
}
class ColumnExpression<TK extends Table, CK extends Column<any>> {
table: TableExpression<TK>
column: CK
alias: string
constructor(table: TableExpression<TK>, column: CK, alias?: string) {
this.table = table
this.column = column
this.alias = alias || column.name
}
eq(val: any): string {
return `${this.table.alias}.${this.column.name} = "${val}"` // Obviously, not safe.
}
}
class E1 extends Table {
name = new StringC('name')
slug = new StringC('slug')
}
let e1 = new E1()
let ee1 = new TableExpression(e1, 'e1')
type TableQuery<TK extends Table> = {
[P in keyof TK]: ColumnExpression<TK, TK[P]>
}
function query<TK extends Table>(te: TableExpression<TK>): TableQuery<TK> {
let result = {} as TableQuery<TK>
for (const k in te.table) {
result[k] = new ColumnExpression(te, te.table[k])
}
return result
}
let tq1 = query(ee1)
console.log(tq1.name.eq('Pancakes')) // e1.name = "Pancakes"
This code compiles and works as expected. What I'm getting stuck on is how to make the eq()
method leverage the generic type used by Column. I can easily extend ColumnExpression to use another type parameter, such as CT like below:
class ColumnExpression<TK extends Table, CT, CK extends Column<CT>> {
...
eq(val: CT) { ... }
}
That part makes sense. The problem then is carrying that forward into the Mapped Type definition for TableQuery
. It seems TableQuery needs to be parameterized in some way that I can pass in a CT to the ColumnExpression but I can't figure out how to do that. Any ideas?
You can use Lookup Types.
To get the generic type as parameter value in eq
change your signature like so:
eq(val: CK['defaultValue']): string
For Column<string>
this will result in string | undefined
, at least with --strictNullChecks
enabled. That's because defaultValue
is optional. So calling eq(undefined)
would still be valid.
You could remove the undefined
by doing CK['defaultValue'] & Object
but I am not sure if this has any side effects that I am not aware of.