I have a module with an object containing a cache of items of different types, each stored in a property of the cache object:
type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}
I have a function to retrieve data from the cache. It takes in a key, representing a property of the cache (i.e. pets or humans), and a generic type specifying the type of data I expect that property to return (i.e. Pet or Human). I can use conditional types to achieve type safety here:
// Alias for all properties of TObj which return a TResult
export type PropertiesOfType<TObj, TResult> = {
[K in keyof TObj]: TObj[K] extends TResult ? K : never
}[keyof TObj]
function readCache<T, K extends PropertiesOfType<Cache, T[]>, C extends { [key in K]: T[] }>(
key: K, cache: C
): T[] {
return cache[key]
}
const pets: Pet[] = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Human[] = readCache("pets", theCache) // Doesn't compile - this is correct
Within the module that all works fine, as I can pass theCache into the readCache function. However, theCache is not exported as I want to keep it private to the module. Instead, I want to export a function that other modules can call to get read-only access to data in the cache, e.g. something like this:
export function getCachedItems<T, K extends PropertiesOfType<Cache, T[]>>(key: K): T[] {
return readCache(key, theCache) // Doesn't compile
}
The problem is that the code above doesn't compile:
TS2345: Argument of type 'Cache' is not assignable to
parameter of type '{ [key in K]: T[]; }'
Is there a way I can get the code to compile (without having to do an unsafe cast)?
UPDATE: Titian Cernicova-Dragomir made two great suggestions:
- Return
Cache[K]. This works fine for places where I want to call the method and know the type I'm working with. But in cases where I have another generic function which wants to call the genericgetCachedItems, and know that what it gets back is an array ofT, that won't work. - Return
Cache[K][number][]. This solves the above problem (and answers the original question). But this only works when the cache contains arrays. What if I want the cache object to be slightly different (more like a Dictionary or Map in other languages): each entry in the cache should itself be another object, with a property for each item which it caches, where the name of the property is some ID of that object, as suggested when designing a Redux store.
In other words I'd have this:
export type Map<T> = { [key: string]: T }
export type Cache = { pets: Map<Pet>, humans: Map<Human> }
const theCache: Cache = {pets: {}, humans: {}}
function readCache<T, K extends PropertiesOfType<Cache, Map<T>>, C extends { [key in K]: Map<T> }>(
key: K, cache: C
): Map<T> {
return cache[key]
}
const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Map<Human> = readCache("pets", theCache) // Doesn't compile - this is correct
export function getCachedItems<T, K extends PropertiesOfType<Cache, Map<T>>>(key: K): Map<T> {
return readCache(key, theCache) // Doesn't compile
}
So I still get the original problem that way. If I try the suggestion of returning C[K], again, it works if I know the type when calling it, but not in a generic function:
function readCache<T, K extends keyof C, C extends { [key in K]: Map<T> }>(
key: K, cache: C
): C[K] {
return cache[key]
}
const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Map<Human> = readCache("pets", theCache) // Doesn't compile - this is correct
export function getCachedItems<T, K extends keyof Cache>(key: K): Cache[K] {
return readCache(key, theCache)
}
const pets2: Map<Pet> = getCachedItems("pets") // Compiles - this is correct
const wrongType2: Map<Human> = getCachedItems("pets") // Doesn't compile - this is correct
function callerInAnotherModule<T, K extends keyof Cache>(key: K) {
const expected : Map<T> = getCachedItems(key) // Doesn't compile
}