Within my codebase, there exists a crucial interface named DatabaseEngine
. This interface utilizes a single type parameter known as ResultType
. This particular type parameter serves as the interface for the query result dictated by the specific database driver. For instance, in the case of MSSQL, it would be represented as the IResult
interface within the context of DatabaseEngine<IResult>
. It's worth noting that this type does not solely pertain to the rowset acquired from a query; instead, it acts as a comprehensive "wrapper" encompassing the rowset along with accompanying metadata such as fields, affected rows, executed SQL statements, and more.
In correspondence to the MSSQL library, the IResult
interface consists of a singular type parameter defining the rows of data being returned. This paradigm appears to be prevalent across many database libraries that I've come across.
The DatabaseEngine
interface features a property named toDataFrame
, which essentially functions as a manipulator to process retrieved data from the database (elaboration on the specifics of this operation is irrelevant for the purpose of this discourse). This function encompasses a type parameter denoted as T, representing the rows of objects extracted from the database. Ideally, its primary parameter should correspond to the result set originating from the database driver (e.g., IResult
). However, since IResult
was previously encapsulated within an individual type parameter, it results in requisite notation like ResultType<T>
, although this approach proves futile.
To summarize concisely: The envisioned type structure can be articulated as follows:
interface DatabaseEngine<ResultType> {
...
toDataFrame<T>(result: ResultType<T>): DataFrame<T>
}
// Here, T signifies the object type present in the rowset obtained from the database, while ResultType indicates the library-specific wrapper for querying outcomes
An illustrative application scenario could be depicted through the following example:
import mssql from 'mssql'
const msSqlDatabaseEngine: DatabaseEngine<IResult> = {
...
toDataFrame<T>: (result) => {
... // Implement operations on the result to return a dataframe
// Thanks to typings, the compiler deduces result as IResult<T>
}
}
const queryResult = mssql.connect({ ... }).query('SELECT * from Employees') // Yields an object embodying the traits of IResult<Employee> (presuming Employee stands as a defined type elsewhere, disregarding further details)
const df = msSqlDatabaseEngine.toDataFrame(queryResult) // Directly utilize queryResult, as the compiler infers the inner generic T to represent Employee during this function invocation
It's evident that this remains an open-ended feature request. Numerous prior Stack Overflow inquiries fail to elucidate this specific requirement where amalgamation of type parameters from distinct origins occurs alongside one serving as a generic. Could there exist a viable workaround or series of utility types accommodating what's being sought after?
My attempt at implementing the aforementioned interface directly yielded errors pointing out that "ResultType is not generic," which aligns with its inherent nature. While trying to make ResultType generic, TypeScript fails to acknowledge this alteration due to its affiliation within a generic scope. Any proposed solutions involving utility types or alternative workarounds capable of yielding similar outcomes are fervently solicited.