Pesquisa de site

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