Reputation: 12804
Title is a bit weird, but hopefully I can explain by example.
I have an object that holds a bunch of SQL queries (defined at compile-time) that looks like this:
const queries = {
getProductById: {
params: { id: 'number' },
sql: `select * from product where id = :id`
},
getCustomerById: {
params: { id: 'number' },
sql: `select * from customer where id = :id`
}
// ... etc ...
};
export { queries };
When I need to use one of these queries in another file, I can import
the queries
object and reference the query by key, which is type-checked by the TypeScript compiler:
// compiles without issues
db.executeQuery(queries.getProductById, { id: 42 });
// compiler error, because "nonexistentQuery" isn't defined
db.executeQuery(queries.nonexistentQuery, { id: 7 });
Now I'd like to be able to define an interface that will add type safety to my queries
variable (in the first example above). I first approached this by creating an interface with an index signature:
interface IQueryList {
[queryName: string]: {
params?: { [paramName: string]: string };
sql: string;
};
}
However, when I add this type annotation to my queries
variable, I lose type safety when referencing a specific query:
const queries: IQueryList = {
getProductById: {
params: { id: 'number' },
sql: `select * from product where id = :id`
}
};
// no compiler error, because "queries" has an index signature
db.executeQuery(queries.nonexistentQuery, { id: 12 });
Is there a way to get the best of both worlds - type safety when defining my query, and safety against referencing a query that isn't defined on the queries
object?
I could accomplish this by tagging each query with its own type annotation, like this:
interface IQuery {
params?: { [paramName: string]: string };
sql: string;
}
const getProductQuery: IQuery = {
params: { id: 'number' },
sql: `select * from product where id = :id`
};
const queries = {
getProductQuery
};
// compiler error, because "nonexistentQuery" doesn't exist
db.executeQuery(queries.nonexistentQuery, { id: 12 });
but I'd prefer to avoid this since each query would need to be tagged individually. In addition, nothing would prevent an incorrectly-formed object from being included in the final queries
object.
Upvotes: 1
Views: 48
Reputation: 249556
You can use a helper function when you define to enforce the constraint, but keep correct checking when using the queries object:
function defineQueries<T extends IQueryList>(q: T) : T {
return q;
}
const queries =defineQueries({
getProductById: {
params: { id: 'number' },
sql: `select * from product where id = :id`
}
});
queries.getProductById; //ok
queries.notAQuery // error
Upvotes: 3