镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

keyof ignores constraint #44985

Description

Bug Report

🔎 Search Terms

keyof, generic constraint

🕗 Version & Regression Information

  • This is a crash ❌ No
  • This changed between versions _______ and _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about keyof
  • I was unable to test this on prior versions because it seems to be consistent behavior in all tested versions

⏯ Playground Link

Playground link with relevant code

💻 Code

// Implementation of event emitter - sample without all methods
type Listener<Event> = (event: Event) => void;

// Here the "key: K" complains "An index signature parameter type must be either 'string' or 'number'"
type Listeners<Events, K extends string & keyof Events> = {[key: K]: Listener<Events[K]>[]}

class EventEmitter<Events extends {[key: string]: unknown}> {
  #listeners: Listeners<Events, keyof Events>

  constructor() {
    this.#listeners = {}
  }
}

// Usage
type MyEvents = {
    connect: void
    reconnect: number
    error: Error
    disconnect: void
}
const emitter = new EventEmitter<MyEvents>()

🙁 Actual behavior

I am getting ts errors even though the types work as expected. See // usage section

🙂 Expected behavior

The keyof returns the same type as the constraint generic and thus will be valid as index signature.

Activity

  1. MartinJohns commented on Jul 12, 2021

    @MartinJohns
    Contributor

    Having keyof T as an indexer type would require #26797.

  2. alesmenzel commented on Jul 12, 2021

    @alesmenzel
    Author

    So, does that mean it is currently not possible to do?
    btw. why is K extends string & keyof Events not enough to make it an indexable type since it requires 'string' or 'number' and the generic K must be a string and also keyof Events?

  3. MartinJohns commented on Jul 12, 2021

    @MartinJohns
    Contributor

    The type string & keyof Events is the same as keyof Events. And a union of keys can not be an index signature. You can use a mapped type instead.

  4. RyanCavanaugh commented on Jul 12, 2021

    @RyanCavanaugh
    Member

    This works without issue

    type Listeners<Events, K extends keyof Events> = Record<K, Listener<Events[K]>>;
  5. alesmenzel commented on Jul 12, 2021

    @alesmenzel
    Author

    Thanks guys,
    the error message wasnt clear to me, but know I understand that you cant use a union of types which you get from the keyof - e.g. type union = 'a' | 'b' | 'c' to index but instead use it in either Record which handles the union case or use mapped type like {[key in K]: ... } instead of {[key: K]:

    Lastly, do you know why there is the last error at the .push(listener) line in .on() method ts playground

    Argument of type 'Listener<Events[Type]>' is not assignable to parameter of type 'Listener<Events[keyof Events]>'.
      Type 'Events[keyof Events]' is not assignable to type 'Events[Type]'.
        Type 'keyof Events' is not assignable to type 'Type'.
          'keyof Events' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint 'string | number | symbol'.
            Type 'string | number | symbol' is not assignable to type 'Type'.
              'string | number | symbol' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint 'string | number | symbol'.
    
                Type 'string' is not assignable to type 'Type'.
                  'string' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint 'string | number | symbol'.(2345)
    

    it seems to me that those types should be the same, no? One is Events[keyof Events] (so basically any values of the Events) and the second one is the concrete event Events[Type] where the type is defined as constraint of keyof Events

      on<Type extends keyof Events>(type: Type, listener: Listener<Events[Type]>): Unsubscribe  {
        if (!type) throw new Error('Cannot call <EventEmitter>.on without a type');
        if (!listener) throw new Error('Cannot call <EventEmitter>.on without a listener');
        this.#listeners[type] = this.#listeners[type] || [];
        this.#listeners[type].push(listener);
        return () => {
          this.off(type, listener);
        }
      }
  6. alesmenzel commented on Jul 12, 2021

    @alesmenzel
    Author

    Looks like I had a mistake in the mapped type, working version for anyone interested
    ts playground

    Thanks Martin Johns (@MartinJohns) and Ryan Cavanaugh (@RyanCavanaugh)

  7. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions