Reputation: 8081
This is the code I have:
type Bucket = {
[key:string]:Map<string,{data:any}>
}
Now what I'm trying to do is to create a type
that would allow me to have custom
data by key. As you can see in the below code, I would like to strongly type the data
object for every key. I've tried by having the generic object on the Bucket
type but that locks all the keys to the same data.
Is there a way to have different strongly typed data
for every key?
const bucket:Bucket = {
a:new Map(),
b:new Map<string,{data:boolean}>()
}
bucket.a.set('a',{data:1})
bucket.b.set('b',{data:'b'}) // this should error
const getA = bucket.a.get('a') // data is any - should be inferred to number?
const getB = bucket.b.get('b') // data is any - should be boolean
Upvotes: 2
Views: 349
Reputation: 10345
When you define a Bucket
type with an indexed signature ({ [x: <key type>] : <value type> }
), you tell the compiler that there is no direct correlation between specific elements and their types. Also, any
is not a "placeholder" type, it opts out of type checking. The compiler thus shows you what you told it, that:
bucket
are of type string
bucket
has values of type Map<string, { data: any }
.You then assign an object to the bucket
variable. At this point, the value is checked for compatibility with the provided type (Bucket
) and since you disabled type checking for data
member, any
thing goes, thus no error.
Next, when you get
value of a member of bucket
, the type is inferred as { data: any; } | undefined
because there is no guarantee that "a"
or "b"
will be present in [x: string]
, hence the value can be undefined
. We already talked about why data
is not narrowed.
If you remove type annotation from the bucket
, you will notice the inference improves drastically because the type is now inferred as well: { a: Map<any, any>; b: Map<string, { data: boolean; }>
. At the same time, you lose type checking for what members of bucket
can contain.
So, what can you do about it? You need to explicitly tell the compiler what is the correlation between certain keys and values. Making your Bucket
generic can help us achieve just that:
type Bucket<T> = {
[ P in keyof T ]: Map<string, { data: T[P] }>
}
Now you have correctly inferred data
types, assurance about the shape of bucket
, and autocompletion at a cost of being more verbose which is a small price to pay for these benefits (besides, you can make the type parameter an interface or a type):
type Bucket<T> = {
[ P in keyof T ]: Map<string, { data: T[P] }>
}
const bucket: Bucket<{ a: number, b: boolean, c?: string }> = {
a:new Map(),
b:new Map(),
};
bucket.a.set('a', { data: 1 })
bucket.b.set('b', { data: 'b' }) // Type 'string' is not assignable to type 'boolean'
const getA = bucket.a.get('a') // data is number
const getB = bucket.b.get('b') // data is boolean
An identity function like the one mentioned by Aleksey L. does the same thing - removes explicit type annotation from bucket
while preserving type-checking (extends Bucket
constraint) and returning a literal type (<T>(a:T) => T
form). It has a cost of emitting a function in compiled code, but it allows you to infer the bucket
type.
Upvotes: 2