1

I have a db-config file in the following form:

const simpleDbConfig: DbConfig = {
    firstTable: {
        columns: {
            foo: { nameDb: "Foo", dataType: "TEXT" },
            bar: { nameDb: "Bar", dataType: "TEXT" },
        },
    },
};

And also the following type definitions:


interface DbConfig {
    firstTable: DbTable<"foo" | "bar">;
}

interface DbTable<T extends string> {
    columns: ColumnObject<T>;
}

type ColumnObject<T extends string> = Record<T, Column>;

interface Column {
    nameDb: string;
    dataType: string;
}

Now what I am trying to do is write a function, that gets the key of a table and an array of keys of columns of this table as well as a property of those columns. It should return an object mapping the column keys to the value of the passed in property of those columns.

Here's the function:


Version 1:

function getColKeyToPropMap<
    TDbTblJsName extends keyof DbConfig,
    TDbTblColObjRecord extends DbConfig[TDbTblJsName]["columns"],
    TDColJsName extends keyof TDbTblColObjRecord,
    TDbColObj extends TDbTblColObjRecord[TDColJsName],
    TColPropName extends keyof TDbColObj
>(tbl: TDbTblJsName, cols: TDColJsName[], colPropName: TColPropName) {
    const tblInfo: TDbTblColObjRecord = simpleDbConfig[tbl].columns as TDbTblColObjRecord;
    const resObj: Record<TDColJsName, TDbColObj[TColPropName]> = {} as Record<TDColJsName, TDbColObj[TColPropName]>;
    for (let col of cols) {
        resObj[col] = tblInfo[col][colPropName];
    }
    return resObj;
}

Version 2:

function getColKeyToPropMap<
    TDbTblJsName extends keyof DbConfig,
    TDbTblColObjRecord extends DbConfig[TDbTblJsName]["columns"],
    TDbColObj extends TDbTblColObjRecord[keyof TDbTblColObjRecord]
>(tbl: TDbTblJsName, cols: Array<keyof TDbTblColObjRecord>, colPropName: keyof TDbColObj) {
    const tblInfo: TDbTblColObjRecord = simpleDbConfig[tbl].columns as TDbTblColObjRecord;
    const resObj: Record<keyof TDbTblColObjRecord, TDbColObj[keyof TDbColObj]> = {} as Record<keyof TDbTblColObjRecord, TDbColObj[keyof TDbColObj]>;
    for (let col of cols) {
        resObj[col] = tblInfo[col][colPropName];
    }
    return resObj;
}





Here is an exemplary function call:

const test = getColKeyToPropMap("firstTable", ["foo"], "nameDb");

// test should be
// {foo : Foo}

I get the following typescript error regarding this line within the for loop: "resObj[col] = tblInfo[col][colPropName];":

"Type 'TColPropName' cannot be used to index type 'TDbTblColObjRecord[TDbColJsName]'."

Where is my mistake?

Thanks for your time and help!

I tried to access nested types with indexed access. Despite the access being valid in javascript, it somehow is not in typescript.

Steffen
  • 13
  • 4
  • Welcome to Stack Overflow! Does this question depend on whatever `SqliteDataTypes`. and `DbTblJsKeys` are? If so, then please define them. If not, then you might want to replace them with native types; either way it would be ideal for your code here to be a [mre] suitable for demonstrating your issue when others paste it into their own TypeScript IDEs. – jcalz Feb 20 '23 at 15:27
  • Extra type parameters just make the job harder for the compiler (but possibly easier for the developer who might like aliases). If you pare down to just those which are necessary (usually no more than one per function parameter) then you get something like [this playground link](https://tsplay.dev/Wo8b8w) where the error goes away. Does that meet your needs? If so I could write up an answer; if not, what am I missing? – jcalz Feb 20 '23 at 15:49
  • Works like a charm, thank you very much. Could you please give me a hint about, what my specific mistake was? – Steffen Feb 20 '23 at 18:06
  • You changed your code out from under me when I made my suggestion, so I'd now need to look at your new code. – jcalz Feb 20 '23 at 18:10
  • Your problem in the current verison is that in `TDbColObj extends TAllColDef[keyof TAllColDef]`, `TDbColObj` can have more properties than `TAllColDef[keyof TAllColDef]` (since, e.g., `{a: string, b: number} extends {a: string}`, so `keyof TDbColObj` might not be a key of `TAllColDef[keyof TAllColDef]`. But now my original suggestion doesn't apply to this version... or rather it does, but now I have to fix extra problems; you essentially did [this](https://tsplay.dev/wXOlLW) which is not what you want. Could you revert to the original version? – jcalz Feb 20 '23 at 18:39
  • Could you maybe help me with another problem please? https://stackoverflow.com/questions/75995416/extracting-information-from-a-nested-db-config-file-accross-various-tables – Steffen Apr 24 '23 at 05:43

1 Answers1

0

Your code is of this form:

function getColKeyToPropMap<
    K extends keyof DbConfig,
    C extends DbConfig[K]["columns"],
    P extends keyof C,
    V extends C[P],
    Q extends keyof V
>(tbl: K, cols: P[], colPropName: Q) {
    const tblInfo: C = simpleDbConfig[tbl].columns as C;
    const resObj: Record<P, V[Q]> = {} as Record<P, V[Q]>;
    for (let col of cols) {
        resObj[col] = tblInfo[col][colPropName];  // error!
    }
    return resObj;
}

and doesn't work because tblInfo[col] is of type C[P] but colPropname is of type Q extends keyof V. And while V is constrained to C[P], that's not the same as saying it is C[P]. V extends C[P] means that V can have many more properties than C[P], and thus a key of V might not be a key of C[P].


This code has more generic type parameters than are needed in your function. Usually you only want no more than a single type parameter per function parameter (there are exceptions but the point is that you want the type arguments to be inferrable from the function arguments directly). If you find yourself wanting to add a type parameter just to be an alias for an annoying-to-write-out type, you can mitigate that by creating an appropriate generic type alias to cut down on some (but probably not all) of the redundancy. For example:

type ColsFor<K extends keyof DbConfig> =
    DbConfig[K]["columns"]

function getColKeyToPropMap<
    K extends keyof DbConfig,
    P extends keyof ColsFor<K>,
    Q extends keyof ColsFor<K>[P]
>(tbl: K, cols: P[], colPropName: Q) {
    const tblInfo: DbConfig[K]["columns"] = simpleDbConfig[tbl].columns;
    const resObj: Record<P, ColsFor<K>[P][Q]> = {} as Record<P, ColsFor<K>[P][Q]>;
    for (let col of cols) {
        resObj[col] = tblInfo[col][colPropName]; // okay
    }
    return resObj;
}

I've used ColsFor<K> to be an alias of DbConfig[K]["columns"] so that doesn't have to show up a bunch of times, and this makes it a little less painful to give up our extra type parameters.

And now the code works; colPropName is of type Q extends keyof ColsFor<K>[P] and tblInfo[col] is of type ColsFor<K>[P]. That means colPropName is known to be a key of tblInfo[col]. And so both sides of that assignment are seen to be of type ColsFor<K>[P][Q] and everything compiles as desired.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360