打字稿使用泛型从使用类型键参数的接口获取类型值?

时间:2021-02-13 15:53:49

标签: typescript generics

我正在努力改进 Typescript 类型,并且需要一个类型安全的事件发射器。我现在已经尝试了很多不同的方法,但我似乎无法完全正确地解析类型。你能看出我哪里出错了吗?

在下面的示例中,我有一个“事件”类型,它将事件名称映射到必须与该事件一起传递的参数。因此,如果我发出“Foo”,我还必须传递一个“bar”字符串,并且侦听器应该知道有一个“bar”属性要读取。

interface Events {
  Foo: {
    bar: string;
  }
}

type EventKeys = keyof Events

class Emitter {
  ...
  emit<K extends EventKeys> (title: K, value: Events[K]): void {
    // With this signature I want to require if the caller specifies a title of "Foo"
    // then they must specify value as "{bar: string}". This part looks to work great!
    this.emitter.emit("connection", [title, value])
  }

  public on (listener: any): void {
    // I use "any" here because this part of the code isn't super relevant to this example
    this.emitter.on('connection', listener.event.bind(f))
  }
}

class Listener {
  ...
  event<K extends EventKeys> ([title, value]: [title: K, value: Events[K]]): void {
    switch(title) {
      case "Foo":
        console.log(value)
        // Here "value" is of type "Events[K]", 
        // which I take to mean it should know it's type "Events[Foo]"
        // or actually "{bar: string}", 
        // but I don't get the autocompletion I expect.

        break
    }
  }
}

难道不能从 {bar: string} 这样的泛型中得到 Events[K] 吗?

1 个答案:

答案 0 :(得分:0)

这里有一些你想要的东西。 emit() 现在是类型安全的,您可以按 Tab 键自动完成可能的事件名称。 需要 TypeScript 4+

演示:https://repl.it/@chvolkmann/Typed-EventEmitter

import { EventEmitter } from 'events'

interface EventTree {
  Connected: {
    foo: string;
  };
}
type EventName = keyof EventTree;

type FormattedEvent<E extends EventName> = [E, EventTree[E]];

class MyListener {
  handleEvent<E extends EventName>([event, args]: FormattedEvent<E>) {
    console.log(`Event "${event}" happened with args`, args);
  }
}

class MyEmitter {
  protected emitter: EventEmitter

  constructor() {
    this.emitter = new EventEmitter()
  }

  emit<E extends EventName>(name: E, args: EventTree[E]) {
    this.emitter.emit('connection', [name, args]);
  }

  registerListener(listener: MyListener) {
    // The typing here is a bit overkill, but for demostration purposes:
    const handler = <E extends EventName>(fEvent: FormattedEvent<E>) => listener.handleEvent(fEvent)
    this.emitter.on('connection', handler)
  }
}
const emitter = new MyEmitter()
emitter.registerListener(new MyListener())

// This supports autocompletion
// Try emitter.emit(<TAB>
emitter.emit('Connected', { foo: 'bar' })

// TypeScript won't compile these examples which have invalid types

// > TS2345: Argument of type '"something else"' is not assignable to parameter of type '"Connected"'.
// emitter.emit('something else', { foo: 'bar' })

// > TS2345: Argument of type '{ wrong: string; }' is not assignable to parameter of type '{ foo: string; }'.
// emitter.emit('Connected', { wrong: 'type' })