Search code examples
typescriptvisual-studio-code

Making typescript recognize index for subsequent parameter


I have a factory that returns a Query Object with structure like this:

query.ts

class Query<T> {
    private queryList: Record<string, string>

    async init(url: string, queries: Record<string, string>) {
        this.queryList = queries
    }
    execute(command: string extends keyof this.queryList, 
        parameter: Record<string, string>
    ): Promise<T[]> {
        
    }
}

export async function InitializeQuery<T>(
    url: string, 
    queries: Record<string, string>
) {
    const queryObject = new Query<T>();
    await queryObject.init(url, queries)
    return queryObject;
}

And a table definition as this:

table.d.ts

type ItemType = {
    id: string,
    name: string,
    weight: number,
    createdBy: string
}

So I execute it like:

index.ts

import { InitializeQuery } from './query.js';

const itemNamedQueries: Record<string, string> = {
    "selectById": "SELECT * FROM Table WHERE id = :id",
    "selectByName": "SELECT * FROM Table WHERE name = :name",
    "findAll": "SELECT * FROM Table"
}

const query = await InitializeQuery<ItemType>(
    'mysql://user:pass@localhost/database', 
    itemNamedQueries);
const result = await query.execute("selectById", {id: 25})

The code is currently works fine, but I found that I made typos here and there regarding itemNamedQuery names.

So, I want that if I run execute(, and press ctrl+space, the IDE would recognize that command parameter from execute method are the keys of that namedQueries and give me a list of possible commands from the index.

How I can achieve that?

UPDATE: I tried making this simple test in VSCode:

const test: Record<string, string> = {
    FindAll: "SELECT * FROM TableData",
    Find: "SELECT * FROM TableData WHERE id = :id",
} as const

function fn<C extends keyof typeof test>(command: C) {

}

fn()

enter image description here

and it didn't work.


Solution

  • When using namedQueries constant, you must not loose its precise type. Currently, you do so by giving it explicit type Record<string, string>

    After that, you can use generics to propagate that type across your functions.

    const namedQueries = {
        "selectById": "SELECT * FROM Table WHERE id = :id",
        "selectByName": "SELECT * FROM Table WHERE name = :name",
        "findAll": "SELECT * FROM Table"
    } as const satisfies Record<string, string>;
    
    export async function InitializeQuery<QUERIES extends Record<string, string>>(url: string, queries: QUERIES) {
        const queryObject = new Query<QUERIES>();
        await queryObject.init(url, queries)
        return queryObject;
    }
    
    class Query<QUERIES extends Record<string, string>> {
        private queryList: QUERIES = null as any;
    
    
        async init(url: string, queries: QUERIES) {
            this.queryList = queries
        }
        execute(command: keyof QUERIES, parameter: Record<string, string>) {
            
        }
    }
    
    const query = await InitializeQuery('mysql://localhost', namedQueries);
    const resultOk = await query.execute("selectById", {id: '25'})   //OK
    const resultBad = await query.execute("selectById1", {id: '25'}) //Expected error
    

    BTW it would make sense to also define accepted parameters in namedQueries and accept only those.