Vitaly Melnikov
Vitaly Melnikov

Reputation: 23

Why typescript enums doesn't helps to infer union type

I got the following class, which is the simple pubsub implementation:

interface IAction<N, T> {
    name: N,
    data: T
}

export class Communicator<A extends IAction<any, any>> {

    public subscribe(
       subscriber: { name: A['name']; handler: (data: A['data']) => void; }
    ) {}

    public emit(name: A['name'], data: A['data']): void {
    }
}

and then I use it like so:

enum ActionName {
    name1 = 'name1',
    name2 = 'name2'
}

type ExampleActions
    = { name: ActionName.name1, data: string }
    | { name: ActionName.name2, data: number };

const communicator = new Communicator<ExampleActions>();

let a = '';

communicator.subscribe({
    name: ActionName.name1,
    /* Expected no error, but get 
       TS2345: Type 'number' is not assignable to type 'string'.*/
    handler: data => a = data
});

/* Expected error Type 'string' is not assignable to type 'number',
    but got no error*/
communicator.emit(ActionName.name2, 'string'); 

Looks like typescript may infer a type of concrete Action through concrete enum, but not. Is it a bug, or maybe I should do something different?

Upvotes: 2

Views: 282

Answers (2)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249556

The problem is that A is a union type so A['data'] will be string | number (all the possibilities in the union). Also there is no inference going on to determine that based on the type of A['name'] the type of data should different, there is no relation between the two other that they are all the possible types for a field on the union.

You can get the desired outcome if you use conditional types in typescript 2.8 (unreleased at the time of writing, but you get it on npm install -g typescript@next, and it will be released in March)

// Helper type TActions will be a union of any number of actions 
// TName will be the name of the action we are intresed in 
// We use filter, to filter out of TActions  all actions that are not named TName 
type ActionData<TActions extends IAction<string, any>, TName> = 
           Filter<TActions, IAction<TName, any>>['data']; 

export class Communicator<A extends IAction<any, any>> {

    public subscribe<K extends A['name']>(
    subscriber: { name: K; handler: (data: ActionData<A, K>) => void; }
    ) {}

    public emit<K extends A['name']>(name: K, data: ActionData<A,K>): void {
    }
}
const communicator = new Communicator<ExampleActions>();

let a = '';

communicator.subscribe({ name: ActionName.name1, handler: d=> a = d}); // ok 
communicator.subscribe({ name: ActionName.name2, handler: d=> a = d}); // erorr as it should be

communicator.emit(ActionName.name1, a); // ok 
communicator.emit(ActionName.name2, a); // erorr as it should be

Upvotes: 1

Bogdan Surai
Bogdan Surai

Reputation: 1275

The reason is a can be just a string, it never should be a number like data can be.

You can write next code:

handler: data => a = data as string

or

handler: data => a = <string>data

Upvotes: 0

Related Questions