Sorry for the strange title, I don't know how to phrase it better.
copy of playground:
class MyClass<T> {
myMap = new Map<K, ((payload: T[K]) => void)[]>(); // How do i get K?
myObj: { [K in keyof T]?: ((value: T[K]) => void)[] } = {}; // This is the same of what i would like to do only that this is an object
myMethod<K extends keyof T>(a: K, cb: (payload: T[K]) => void) { // this signature is what i would like to have!
// ...checks etc.
// add to myMap
let entry = this.myMap.get(a);
entry?.push(cb);
}
}
So I have a class Foo and inside this class, I have a property of type Map<,> and now I would like to fill in these generics based on T. I have a method on this class where this works as expected because there I can introduce a second generic 'K' but I don't know how I can do that for my Map<,>.
I was able to get it somehow working with an object but not with a Map<,>
All ideas are highly appreciated!
CodePudding user response:
Unfortunately, there is no equivalent of Mapped Types for ES6 Map
class (despite the name...)
This is because TypeScript expects Map
to contain homogeneous type of values: https://github.com/microsoft/TypeScript/blob/v4.9.3/lib/lib.es2015.iterable.d.ts#L119
Therefore you should just stick with your myObj
map object, if you want to handle heterogeneous types of values.
CodePudding user response:
Short answer: It's not possible with Map.
Long Answer
The important thing to understand here is the differences between a vanilla javascript Object is versus the Map object.
Vanilla Javascript Objects
With javascript Objects, they can represent basically anything. They can represent a "static" object with a strict set of keys with corresponding values. A Typescript definition that represents this would be:
type MyObject = { name: string, age: number}
// an example of an object that fits the shape of the type definition
const myObject: MyObject = { name: 'John', age: 30 };
This represents an object with "static" keys where you always expect that object to have 2 keys, where name is a string
, and age is number
, nothing more nothing less.
A plain javascript Object could also represent a set of key value pairs. A typescript definition of an object like this could look like:
type MyObject = { [key: string]: number };
const myObject: MyObject = {
anyKeyHereIsValid: 5,
orAKeyThatLooksLikeThis: 6,
' even this key with spaces': 8,
anyNumericValueIsValidToo: Math.PI
}
Javascript "Map" Object
Compare the a vanilla javascript object this to a Javascript Map
object which has a much more strictly defined definition. The Map object is a representation of the "Hash Map" data structure.
"Map objects are collections of key-value pairs. A key in the Map may only occur once; it is unique in the Map's collection. A Map object is iterated by key-value pairs" (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
It is possible to represent a Hash Map DS with a vanilla Javascript object, but the link above lists several reasons why you would want to use the Map object directly instead (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps).
Tying this into Typescript
The type { [K in keyof T]?: ((value: T[K]) => void)[] }
is a Mapped Type (https://www.typescriptlang.org/docs/handbook/2/mapped-types.html). Mapped Types are very generic. Here "generic" means that you can create a mapped type over any type definition that represents a vanilla javascript object.
On the other hand, the Map object is more constrained, it should be able to hold an arbitrary amount of keys, all with the same value. By definition, different keys should not map to different type definitions.
Therefore, if you are trying to do this:
// this original
type Person = {
name: string,
age: number;
}
// converted into this computed type
type ComputedPerson = {
name: ((value: string) => void))[],
age: ((value: number) => void))[]
}
It would not make sense to put into a vanilla JS object because you don't have an arbitrary number of keys that map to the same type of value.
It would only make to use a Map if your type definition looked like this instead:
type Person = {
[key in 'name' | 'age']: number | string;
}
type ComputedPerson = {
[Key in 'name' | 'age']: ((value: number | string) => void)[]
}
// in this case, you could define a Map like this:
const Map: Map<keyof Person, ((value: Person[keyof Person]) => void)[]> = new Map();
// you could then generalize to:
type Mapper<T> = Map<keyof T, T[keyof T]>;
type ComputedPerson2 = Mapper<Person> // this is equivalent to ComputedPerson
My guess, however, is that this isn't what you're trying to do since you're looking for type safety on specific keys and its corresponding values. Here, it's really important to pick the right data type for your use case. It doesn't seem like you should be using a Map so I would just stick to using the plain javascript objects and Mapped Types.