Typescript: Kartlagd returtyp baserad på nycklar som finns i argumentet


Låt oss säga att jag skriver ett databassnitt med vilket jag kan skicka flera förfrågningar på en gång som ett objekt med tabellnamn som nycklar, och förvänta mig att få ett objekt med resultat på samma nycklar som fanns i förfrågan.

interface Query {
    users?: UsersQuery
    products?: ProductsQuery
    orders?: OrdersQuery
}
interface Result {
    users?: User[]
    products?: Product[]
    orders?: Order[]
}

declare function readData(query: Query): Result;

Finns det något sätt att vara mer specifik om returtypen baserat på vilka nycklar som faktiskt finns i det passerade objektet?

Det verkar som att jag är ganska nära med något som detta:

declare function readData<T extends Query>(query: T): { [K in keyof T]: T[K] }

Men istället för den mappade returtypen som använder T[K], behöver jag att den använder Result[K], vilket inte kompilerar (med error TS2536: Type 'K' cannot be used to index type 'Result'). Baserat på dokumentationen antar jag att det beror på att allt annat än T[K] inte är homomorfiskt, även om jag inte är helt säker på det.

Är det något jag missar här när jag använder mappade typer, eller finns det något annat sätt att göra detta på?

Svar #1

En av de mer oväntade konsekvenserna av hur typer fungerar i detta fall är att eftersom T utvidgar Query kan det inte bara ha egenskaper från Query, utan så länge det finns viss överlappning kan det ha vilken nyckel som helst som kanske inte är önskvärd för att kontrollera om frågeobjektet är korrekt:

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
})

Detta är också anledningen till felet du får T kan ha andra nycklar utöver de i frågan.

Ett alternativ är att ange att argumentet till funktionen är en Pick av Query med nycklarna som väljs som typparametern till funktionen:

declare function readData<K extends keyof Query>(query: Pick<Query, K>): { [P in K]: Result[K] }


readData({
    users: null!,
    bla: 0 // error now
})

Även om detta typkontrolleras som förväntat är problemet att det inte kommer att erbjuda kodkomplettering på nycklarna i objektliteralet, vilket är olyckligt.

Om vi lägger till en intersection med partial av Query, får vi tillbaka bra kodkomplettering och vi fångar i K de faktiska nycklarna som skickas in (även om de kan vara undefined, men du har förmodligen redan kontrollerat det)

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
})

Svar #2

Börja med att generalisera UserQuery, ProductQuery osv. för att använda en gemensam, generisk basklass. Till exempel:

// 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;

Definiera sedan ditt Query-gränssnitt så här:

interface Query {
    users?: EntityQuery<User>
    products?: EntityQuery<Product>
    orders?: EntityQuery<Order>
}

Nu kan du göra readData generisk på detta sätt:

declare function readData<T extends Query>(query: T): { [K in keyof T]: ResultOf<T[K]> };