Por que os enums do TypeScript não ajudam a inferir o tipo união
Eu tenho a seguinte classe, que é a implementação simples de pubsub:
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 {
}
}
e então eu uso assim:
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');
Parece que o TypeScript pode inferir um tipo de Action concreto através de um enum concreto, mas não. É um bug, ou talvez eu deva fazer algo diferente?
Resposta #1
O problema é que A é um tipo união, então A['data'] será string | number (todas as possibilidades na união). Além disso, não há inferência ocorrendo para determinar que, com base no tipo de A['name'], o tipo de data deveria ser diferente; não há relação entre os dois além de serem todos os tipos possíveis para um campo na união.
Você pode obter o resultado desejado se usar tipos condicionais no TypeScript 2.8 (não lançado na época da escrita, mas você pode obtê-lo com npm install -g typescript@next, e será lançado em março)
// 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
Resposta #2
A razão é que a pode ser apenas uma string, ele nunca deveria ser um número como data pode ser.
Você pode escrever o próximo código:
handler: data => a = data as string
ou
handler: data => a = <string>data
