3

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?

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

That solution seems to work. I added an additional value to the Column<T> class base: T and then used a Type Lookup on base to get the T.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.