Urastor
Urastor

Reputation: 91

How to write "K extends keyof T" in a generic class and use K then

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

Answers (2)

jcalz
jcalz

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);

Playground link

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

Christian Ivicevic
Christian Ivicevic

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

Related Questions