我有一个模块,该模块的对象包含一个不同类型的项的缓存,每个项存储在缓存对象的属性中:
type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}
我有一个从缓存中检索数据的功能。它接收一个代表缓存属性的键(即pets
或humans
)和一个通用类型,该类型指定我希望该属性返回的数据类型(即Pet
或Human
)。我可以在这里使用条件类型来实现类型安全:
// 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
在该模块中所有功能均正常运行,因为我可以将theCache
传递到readCache
函数中。但是,theCache
并未导出,因为我想对模块保密。相反,我想导出一个其他模块可以调用的函数,以获取对缓存中数据的只读访问权限,例如像这样的东西:
export function getCachedItems<T, K extends PropertiesOfType<Cache, T[]>>(key: K): T[] {
return readCache(key, theCache) // Doesn't compile
}
问题是上述代码无法编译:
TS2345: Argument of type 'Cache' is not assignable to
parameter of type '{ [key in K]: T[]; }'
有没有一种方法可以使代码编译(而不必进行不安全的强制转换)?
更新:Titian Cernicova-Dragomir提出了两个重要建议:
Cache[K]
。这在我想调用该方法并知道我正在使用的类型的地方效果很好。但是,如果我有另一个泛型函数想要调用泛型getCachedItems
,并且知道它返回的是T
数组,那是行不通的。Cache[K][number][]
。这样可以解决上述问题(并回答原始问题)。但这仅在缓存包含数组时有效。如果我希望缓存对象稍有不同(更像是其他语言的词典或地图),该怎么办:缓存中的每个条目本身应该是另一个对象,为其缓存的每个项目都具有一个属性,其中该属性的名称是该对象as suggested when designing a Redux store的某个ID。换句话说,我会得到这个:
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
}
所以我仍然可以通过这种方式得到最初的问题。如果我尝试返回C[K]
的建议,那么,如果我在调用它时知道类型,那是可行的,但是在通用函数中却不是:
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
}
答案 0 :(得分:1)
仍然包含未解析类型参数的条件类型通常是一个问题,因为编译器无法扩展它们。
如果cache
仅包含相关键,一种更好的方法是使用索引类型查询,在这种情况下它们会产生相同的结果,但对编译器更友好。
type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}
function readCache<K extends keyof C, C extends { [key in K]: any[] }>(
key: K, cache: C
): C[K] {
return cache[key]
}
const pets: Pet[] = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Human[] = readCache("pets", theCache)
export function getCachedItems<K extends keyof Cache>(key: K): Cache[K] {
return readCache(key, theCache) // ok
}
尽管上述答案有效,但注释中的请求应能够将readCache
的结果视为数组。尽管Cache[K]
是数组的并集,但其方法很复杂且难以使用。我们可以进一步向下钻取一层,并使用Cache[K]
从Cache[K][number]
获取项目类型,并将其用作结果中的数组项目。这将使数组在getCachedItems
之类的方法中工作良好:
type Pet = { id: string; name: string, petFood: string }
type Human = { id: string; firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}
function readCache<K extends keyof C, C extends { [key in K]: any[] }>(
key: K, cache: C
): C[K][number][] {
return cache[key]
}
const pets: Pet[] = readCache("pets", theCache) // Compiles - this is correct
export function getCachedItems<K extends keyof Cache>(key: K): Cache[K][number][] {
return readCache(key, theCache) // ok
}
export function getCachedItemsAndMap<K extends keyof Cache>(key: K) {
return readCache(key, theCache)
.map(o => ({ // map works fine
id: o.id, // we can even access common members
item: o
}));
}
修改
问题中添加了Map
类型的版本:
type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
export type Map<T> = { [key: string]: T }
export type Cache = { pets: Map<Pet>, humans: Map<Human> }
const theCache: Cache = {pets: {}, humans: {}}
function readCache<K extends keyof C, C extends { [key in K]: Map<any> }>(
key: K, cache: C
): Map<C[K][string]> {
return cache[key]
}
const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct
export function getCachedItems<K extends keyof Cache>(key: K): Map<Cache[K][string]> {
return readCache(key, theCache) // ok
}
export function getCachedItemsAndMap<K extends keyof Cache>(key: K) {
let cache: Map<Cache[K][string]> = readCache(key, theCache)
}
编辑v2
实际上,我认为不需要全部,因为您要访问的Map
上没有相关方法。这也可以:
type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
export type Map<T> = { [key: string]: T }
export type Cache = { pets: Map<Pet>, humans: Map<Human> }
const theCache: Cache = {pets: {}, humans: {}}
function readCache<K extends keyof C, C extends { [key in K]: Map<any> }>(
key: K, cache: C
): C[K] {
return cache[key]
}
const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct
export function getCachedItems<K extends keyof Cache>(key: K): Cache[K] {
return readCache(key, theCache) // ok
}
export function getCachedItemsAndMap<K extends keyof Cache>(key: K) {
let cache: Cache[K] = readCache(key, theCache)
let a = cache[''] // Pet | Human
}