Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript 2.8 Conditional Type Issue

I've been experimenting with TypeScript 2.8's conditional types after getting stuck on a problem in TS 2.6 but I've run into an error that I don't understand and could use some help:

The example is quite involved to get at the issue.

export class Column<T> {
  readonly base: T

  constructor(base: T) {
    this.base = base
  }
}

export class ColumnExpression<E, CT, CK extends Column<CT>> {
  readonly table: TableExpression<E>
  readonly column: CK
  alias?: string

  constructor(table: TableExpression<E>, column: CK, alias?: string) {
    this.table = table
    this.column = column
    this.alias = alias
  }
}

type Table<E> = {
  [P in keyof E]: E[P] extends (infer U) ? Column<U> : never
}

type TableQuery<E> = {
  [P in keyof E]: E[P] extends Column<(infer CT)> ? ColumnExpression<E, CT, E[P]> : never
}

export class TableExpression<E> {
  readonly table: Table<E>
  alias?: string

  constructor(table: Table<E>, alias?: string) {
    this.table = table
    this.alias = alias
  }
}

function toTable<E>(target: E): Table<E> {
  let result = {} as Table<E>
  for (const k in target) {
    result[k] = new Column(target[k])
  }
  return result
}

function toTableExpression<E>(target: E): TableExpression<E> {
  const table = toTable(target)
  return new TableExpression(table)
}

function toTableFilter<E>(target: TableExpression<E>): TableQuery<E> {
  let result = {} as TableQuery<E>
  let table = target.table
  for (const k in table) {
    result[k] = new ColumnExpression(target, table[k])
  }
  return result
}

class Test {
  id: number
  name: string
  createdAt: Date
  createdById: number
}

let contentTable = toTable(new Test())
let contentFilter = toTableFilter(new TableExpression(contentTable))

contentTable.id.base
contentTable.name.base

The exact error pops up on line 56:

result[k] = new ColumnExpression(target, table[k])

The error:

(56,46): error TS2345: Argument of type 'Table<E>[keyof E]' is not assignable to parameter of type 'Column<{}>'.
  Type 'Column<E[keyof E]>' is not assignable to type 'Column<{}>'.
    Type 'E[keyof E]' is not assignable to type '{}'.
1:57:46 PM - Compilation complete. Watching for file changes.

In TS 2.6, I used a different type definition for ColumnExpression based on an indexed type access (on the advice of someone else on SO), for reference, it was defined as:

type TableQuery<TK extends TableLike> = {
  [P in keyof TK]: Column.ColumnExpression<TK, TK[P]['base'], TK[P]>
}

The TK[P]['base'] allowed me to get to the underlying column type but this approach no longer works as of TS 2.7. It is unclear whether it is a bug or not. I've read through some GH issues that seem related but none that tackle this exact issue. I was hoping the conditional types introduced in 2.8 would allow me to solve this issue more cleanly but so far, not much luck.

Any thoughts would be appreciated.

like image 266
Rob Avatar asked Nov 29 '25 04:11

Rob


1 Answers

As I said, your types seem to be broken and I don't know what they're supposed to be. If your code works at runtime (does it?) then the following code produces no errors, seems to be consistent with what you're doing at runtime, and uses no conditional types (so should work with TS2.6):

export class Column<T> {
  readonly base: T

  constructor(base: T) {
    this.base = base
  }
}

// remove CT type, it can be calculated as CK['base'] if needed
export class ColumnExpression<E, CK extends Column<any>> {
  // type CT = CK['base']
  readonly table: TableExpression<E>
  readonly column: CK
  alias?: string

  constructor(table: TableExpression<E>, column: CK, alias?: string) {
    this.table = table
    this.column = column
    this.alias = alias
  }
}

// no need for conditional types, reduces to this:
type Table<E> = {
  [P in keyof E]: Column<E[P]>
}

// no need for conditional types, reduces to this:
type TableQuery<E extends Table<any>> = {
  [P in keyof E]: ColumnExpression<E, E[P]>
}


export class TableExpression<E> {
  readonly table: Table<E>
  alias?: string

  constructor(table: Table<E>, alias?: string) {
    this.table = table
    this.alias = alias
  }
}

function toTable<E>(target: E): Table<E> {
  let result = {} as Table<E>
  for (const k in target) {
    result[k] = new Column(target[k])
  }
  return result
}

function toTableExpression<E>(target: E): TableExpression<E> {
  const table = toTable(target)
  return new TableExpression(table)
}

// this is the real output of toTableFilter();
// note the difference between TableThing<E> and TableQuery<E>:
type TableThing<E> = {
  [P in keyof E]: ColumnExpression<E, Column<E[P]>>
}


function toTableFilter<E>(target: TableExpression<E>): TableThing<E> {
  let result = {} as TableThing<E>;
  let table = target.table
  for (const k in table) {
    const z = new ColumnExpression(target, table[k])
    result[k] = z
  }
  return result
}

class Test {
  id!: number
  name!: string
  createdAt!: Date
  createdById!: number
}

let contentTable = toTable(new Test())
let contentFilter = toTableFilter(new TableExpression(contentTable))

contentTable.id.base
contentTable.name.base

This all type checks and should emit the same JavaScript as the problematic code from your question. Honestly I'm not sure if the code does what it's supposed to do at runtime, or what TableThing<E> actually means. I think that might be up to you to figure out.

Hope that was some help. Good luck!

like image 91
jcalz Avatar answered Nov 30 '25 20:11

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!