Reputation: 23
Let's say I'm writing some database interface with which I can submit multiple queries at once as an object keyed by table names, and expect to receive an object with results at the same keys that were in the query.
interface Query {
users?: UsersQuery
products?: ProductsQuery
orders?: OrdersQuery
}
interface Result {
users?: User[]
products?: Product[]
orders?: Order[]
}
declare function readData(query: Query): Result;
Is there any way to be more specific about the return type based on which keys are actually present in the passed in object?
It seems like I'm pretty close with something like this:
declare function readData<T extends Query>(query: T): { [K in keyof T]: T[K] }
But instead of the mapped return type using T[K]
, I need it to use Result[K]
, which doesn't compile (with error TS2536: Type 'K' cannot be used to index type 'Result'
). Based on the docs, I'm guessing it's because anything other than T[K]
isn't homomorphic, though I'm not super clear on that.
Is there something I'm missing here in using mapped types, or is there some other way to do this?
Upvotes: 2
Views: 228
Reputation: 669
You're on the right track!
I'd do something like this:
// Create a mapping between the query type used, and the result type expected:
type QueryResult<T> =
T extends UsersQuery ? User[] :
T extends ProductsQuery ? Product[] :
T extends OrdersQuery ? Order[] :
never
;
// Same as what you started with, but using our new QueryResult<T> mapping:
declare function readData<T extends Query>(query: T): { [K in keyof T]: QueryResult<T[K]> };
And that should give you the correct typings based on which things you pass in.
For reference, I tested this with the following interfaces:
interface UsersQuery {
user_id: string;
}
interface ProductsQuery {
product_id: string;
}
interface OrdersQuery {
order_id: string;
}
interface User {
_id: string;
userName: string;
}
interface Product {
_id: string;
productName: string;
}
interface Order {
_id: string;
orderName: string;
}
interface Query {
users?: UsersQuery;
products?: ProductsQuery;
orders?: OrdersQuery;
}
Upvotes: 0
Reputation: 250406
One of the more unexpected consequences of how types work in this case is that since T extends Query
it can have not just properties of Query
, but, as long as there is some overlap, it can have any key which might not be desirable for checking if the query object is correct:
declare function readData<T extends Query>(query: T): { [K in keyof T]: T[K] }
readData({
bla: 1, // not in Query but the compiler is fine, no excess property checks since T just has to extend Query
orders: null
})
This is also the reason for the error you are getting T
may have other keys beside those in query.
One option is to specify the argument to the function is a Pick
of Query
with the keys being picked being the type parameter to the function:
declare function readData<K extends keyof Query>(query: Pick<Query, K>): { [P in K]: Result[K] }
readData({
users: null!,
bla: 0 // error now
})
While this does type check as expected the problem is it will not offer code completion on the keys of the object literal, which is unfortunate.
If we add an intersection with the partial of Query
, we get back good code completion and we capture in K
the actual keys passed in (although they can de undefined, but you probably were already checking for that)
declare function readData<K extends keyof Query>(query: Pick<Query, K> & Partial<Query>): { [P in K]: Result[K] }
readData({
users: null!,
// we get suggestions here
})
Upvotes: 1
Reputation: 149108
Start by generalizing the UserQuery
, ProductQuery
, etc. to use a common, generic base type. For example:
// Entity definitions
type User = { userId: string };
type Product = { productId: string };
type Order = { orderId: string };
type EntityQuery<T> = { [K in keyof T]: T[K] }
type ResultOf<T> = T extends EntityQuery<infer I> ? I[] : undefined;
Then define your Query
interface like this:
interface Query {
users?: EntityQuery<User>
products?: EntityQuery<Product>
orders?: EntityQuery<Order>
}
Now you can make readData
as generic like this:
declare function readData<T extends Query>(query: T): { [K in keyof T]: ResultOf<T[K]> };
Upvotes: 1