Royi Namir
Royi Namir

Reputation: 148514

Typescript generics is rejected when specifying `keyof?

I have a generic method which is from the RXJS library (here simplified) :

function Subject<T>(t: T):T {
    return t;
}

I also have an interface which declares my app values. (keys can be added)

interface IState { 
    key1: number;
    key2: string;
}

And finally I have a Store which applies to the IState interface with actual values, via the generic function wrapper ( the generic function is an alias to the RXJS subject)

let Store  : IState=  Subject<IState>({  
    key1: void 0,
    key2: void 0,

})

Ok so lets add 2 method for getting & setting from store :

Setting to store :

 function set<T>(name: keyof IState, statePart: T) {
        Store={
            ...Store,
            [name]: statePart 
        };

Usage : set<string>("key1", "3");

This function works fine and allows me to only use valid keys which belongs to IState. No errors here.

But looking at the Select method :

(invocation should be like: )

let myVal:number = select<number>("key1");

Here is the method :

function select<T>(k: keyof IState): T { 
    return <T>Store[k];  // <-- error here
}

Type 'string | number' cannot be converted to type 'T'. Type 'number' is not comparable to type 'T'.

Question:

Why is that ? If I remove the keyof :

function select<T>(k): T { 
    return <T>Store[k];
}

Then it does compile , but it's not making any sense , Store is type of Istate and Istate contains keys of Istate

Why does it work without keyof and how can I fix my code so that select method will force to select only keys of Istate ?

ONLINE DEMO

Upvotes: 3

Views: 177

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

The problem is that k can be any key of IState so there is no way to make sure that Store[k] is compatible with T. I would change the generic parameter to be the key, and type the result in relation to the field type:

function select<K extends keyof IState>(k: K): IState[K] { 
    return Store[k];
}
let myVal  = select("key1"); // myval is number

Also set can be improved to not have explicit generic parameters, and to ensure the value passed to set is compatible with the field type:

function set<K extends keyof IState>(name: K, statePart: IState[K]) {
    Store={
        ...Store,
        [name]: statePart 
    }
}

set("key1", 3);

Edit

As mentioned in the comments, you can see the actual inferred types if you hover over the call (at least in vscode):

enter image description here

If you want to keep the explicit type parameters, although I do not recommend this as you can easily mismatch the field type and the call type you can use a type assertion through any:

function select3<T>(k: keyof IState): T { 
    return Store[k] as any;
}

Upvotes: 3

Related Questions