Reputation: 23
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
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
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