Typescript: Cast problem with generics method definition

Created on 20 Dec 2017  路  4Comments  路  Source: microsoft/TypeScript

TypeScript Version: 2.6.2

Code

enum ActionType {
    Call
}

interface Action {
    type: ActionType;
}

interface CallAction extends Action {
    context: any;
}

class Service {
    exec<T extends Action>(action: T) {
        if (action.type == ActionType.Call) {
            (<CallAction>action).context = {};
        }
    }
}

Expected behavior:

I should use cast because CallAction extends Action

Actual behavior:

I have the following compilation error:

(<CallAction>action).context = {};
Type 'T' cannot be converted to type 'CallAction'.
Type 'T' cannot be converted to type 'CallAction'.
  Type 'Action' is not comparable to type 'CallAction'.
    Property 'context' is missing in type 'Action'.
Committed Suggestion

Most helpful comment

@DanielRosenwasser Over to you - we agreed that this should be allowed in the comparability relation assuming it can be reasonably spec'd.

All 4 comments

Could you elaborate on why using Action instead of T isn't sufficient in this case?

I have an other class Store

class Store {
    db = new Map<ActionType, Action[]>();
    service = new Service();
    store(action: Action) {
        if (this.db.has(action.type)) {
            this.db.set(action.type, []);
        }
        this.db.get(action.type).push(action);
        this.service.exec(action);
    }
    storeGeneric<T extends Action>(action: T) {
        if (this.db.has(action.type)) {
            this.db.set(action.type, []);
        }
        this.db.get(action.type).push(action);
        this.service.exec(action);
    }
}
const store = new Store();

This example doesn't work

store.store({
    type: ActionType.Call,
    context: {}
});
Argument of type '{ type: ActionType; context: {}; }' is not assignable to parameter of type 'Action'.
  Object literal may only specify known properties, and 'context' does not exist in type 'Action'.

This example works

store.storeGeneric<CallAction>({
    type: ActionType.Call,
    context: {}
});

That's why I use T instead Action in my Service. I see there is no problem when I switch to a non Generic method. But I want to know why it doesn't work, it's a non intuitive behavior...

I solved the problem using generic class instead of generic methods

interface Typed<U> {
    type: U;
}

interface Service<T extends Typed<U>, U> {
    exec(action: T): T;
}

class Store<T extends Typed<U>, U> {
    db = new Map<U, T[]>();
    constructor(private service: Service<T, U>) {}
    store(action: T): T {
        if (this.db.has(action.type)) {
            this.db.set(action.type, []);
        }
        this.db.get(action.type).push(action);
        this.service.exec(action);
        return action;
    }
}


enum ActionType {
    Call
}

interface Action extends Typed<ActionType>{
    type: ActionType;
}

interface CallAction extends Action {
    context: any;
}

class ActionService implements Service<Action, ActionType> {
    exec(action: Action) {
        if (action.type === ActionType.Call) {
            console.log('ActionType.Call', (<CallAction>action).context);
        }
        return action;
    }
}
const service = new ActionService();
const store = new Store<Action, ActionType>(service);
store.store({
    type: ActionType.Call
});

@DanielRosenwasser Over to you - we agreed that this should be allowed in the comparability relation assuming it can be reasonably spec'd.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments

blendsdk picture blendsdk  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments