I'm trying to create an object where each property is mapped to a class instance. The class type relies on two generics. When I access a property in my object, TypeScript infers the type of the property as the union type used to define the object rather than the type of the property itself.
How can I get TypeScript to infer the type of the property properly?
Here is a code snippet to help clarify the problem:
export class CacheWithExpiry<T extends CacheTypes, V extends CacheNames> {
constructor() {
}
...
}
export type CacheNames =
'foo_1' |
'foo_2'
type CacheTypes = string | number
type CacheMap<T extends CacheTypes, V extends CacheNames> = { [key: string]: CacheWithExpiry<T, V> }
export const cacheMap: CacheMap<CacheTypes, CacheNames> = {
foo1Cache: new CacheWithExpiry<string, 'foo_1'>(), // Type is inferred as expected 'CacheWithExpiry<string, "foo_1">'
foo2Cache: new CacheWithExpiry<number, 'foo_2'>()
} as const
cacheMap.foo1Cache// Type is inferred as 'CacheWithExpiry<CacheTypes, CacheNames>' instead of 'CacheWithExpiry<string, "foo_1">'
Edit 1: I also need to maintain type safety for cacheMap
so removing the type completely from cacheMap
wouldn't work. Example:
export const cacheMap = {
foo1Cache: new CacheWithExpiry<string, 'foo_1'>(),
foo2Cache: new CacheWithExpiry<number, 'foo_2'>(),
foo3: 'I am not a cache class instance', // I still want this to throw an error
} as const
CodePudding user response:
You assigned the type CacheMap<CacheTypes, CacheNames>
to the variable cacheMap
. This type does not hold any information about the keys foo1Cache
or foo12ache
. All it knows is that there could be any key of type string
which would hold a value of type CacheWithExpiry<CacheTypes, CacheNames>
. That is why when you access a property of cacheMap
, TypeScript will not know about the specific properties.
If you take away the type though, TypeScript will know the properties and their types:
export const cacheMap = {
foo1Cache: new CacheWithExpiry<string, 'foo_1'>(),
foo2Cache: new CacheWithExpiry<number, 'foo_2'>()
} as const
cacheMap.foo1Cache // foo1Cache: CacheWithExpiry<string, "foo_1">
The type of cacheMap
is now:
const cacheMap: {
readonly foo1Cache: CacheWithExpiry<string, "foo_1">;
readonly foo2Cache: CacheWithExpiry<number, "foo_2">;
}
If you also want type validation when creating the cacheMap
object, you need to pass the object literal to a generic function.
function createCacheMap<T extends CacheMap<CacheTypes, CacheNames>>(cacheMap: T): T {
return cacheMap
}
export const cacheMap = createCacheMap({
foo1Cache: new CacheWithExpiry<string, 'foo_1'>(),
foo2Cache: new CacheWithExpiry<number, 'foo_2'>(),
anything: "string" // Error: Type 'string' is not assignable to type 'CacheWithExpiry<CacheTypes, CacheNames>
})
cacheMap.foo1Cache // foo1Cache: CacheWithExpiry<string, "foo_1">