Search code examples
typescriptfunctiongenericsindexed

Problem with indexed access within nested generics


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.


Solution

  • 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