Reputation: 91
I want to create a class, which gets an object as type parameter and then uses the type as a type of a property in that class, so that I can only set and get values, if that key exists in that object.
I create an interface, lets call it IStorage. A class Storage should implement iStorage and store values based on if the key exists in the given object.
interface iStorage<T, K extends keyof T> {
getField(k: K): T[K];
setField(k: K, v: string | number | object): T[K];
render(): void;
}
class Storage<T, K extends keyof T> implements iStorage<T, K> {
private fields: { [key: K]: string | number | object };
public getField(k: K): T[k] {
return this.fields[k];
}
public setField(k: K, v: string | number | object): T[k] {
return (this.fields[k] = v);
}
}
let storage = new Storage<{name: string, type: number}>();
I want fields in Storage to match the the generic defintion (I think T[K]), but typescript says that key must be of type string or number, so this is not working as expected. In my interface I would like to say, that the second parameter of setFormField is also T[K]. I also think, that my type definition is repetitive. Typescript wants me to give a seconds argument too, which makes sense to me, but I don't want to write K extends keyof T everytime on a function, nor could I then use it as a property type.
Tried quiet a lot, but couldn't get it to work.. I'm a bit lost actually, since I'm trying this for several hours now. Never had to do with generics before, so please be gentle on me. :D
Hope someone could give me a helping hand.
Upvotes: 1
Views: 3771
Reputation: 329573
I'm not 100% sure about your use case, but I'd be inclined to alter your code to something like this:
interface IStorage<T extends object> {
getField<K extends keyof T>(k: K): T[K];
setField<K extends keyof T>(k: K, v: T[K]): T[K];
render(): void;
}
class Storage<T extends object> implements IStorage<T> {
constructor(private fields: T) { }
render() { }
public getField<K extends keyof T>(k: K) {
return this.fields[k];
}
public setField<K extends keyof T>(k: K, v: T[K]): T[K] {
return (this.fields[k] = v);
}
}
let storage = new Storage({ name: "str", type: 123 });
const str = storage.getField("name");
const num = storage.setField("type", 456);
Here the IStorage
interface only depends on the type T
of the object, and the individual getField()
and setField()
methods are themselves generic functions which can act on any property key K
from keyof T
. The generic type parameter K
does not appear on the interface itself.
I've changed the implementation of Storage
to match. Note that I've made the constructor take a value of type T
which is stored as a private member. If you don't want to have to pass in an initial value, then you will have to restrict T
to a type with all optional properties (that is, using something like Partial<T>
instead of just T
.)
And you can see from the code above (or in the playground link) that the IntelliSense is how you want it... calling getField("name")
is known to return a value of type string
.
Okay, hope that helps you. Good luck!
Upvotes: 2
Reputation: 10895
Your key type [key: K]
cannot be handled by Typescript. What would two objects as keys be like? When are they considered equal? Hence the type of Records/Dictionary keys must be either string, number or symbol and nothing else.
You might be able to mimick what you're attempting by using an array of tuples such as Array<[KeyType, ValueType]>
and filter by first element of the tuples.
Upvotes: 0